webpack4篇(上)

296 阅读21分钟

什么是webpack

webpack4 可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JS模板以及其他的一些浏览器不能直接运行的拓展语言(Scss, Ts)等,并将其打包为合适的格式以供浏览器使用。

webpack可以做什么

  • 代码转换: es6转es5,sccc、less转css等
  • 文件优化: 压缩代码体积,合并文件,摇树优化等
  • 代码分割: 公共模块抽离,路由懒加载等
  • 模块合并: 多个模块合并成一个模块,按功能来分类
  • 自动刷新: 自己启动一个本地服务,来实现代码变更后,可以更新我们的页面
  • 代码校验: 校验代码是否符合规范
  • 自动发布: 打包结果发布到服务器上,后面提到的方法

webpack安装

安装本地的webpack

yarn add webpack@^4.32.2 webpack-cli@^3.3.2 -D

// 可以查看 npm 源
yarn global add nrm

// 查看 npm 源
nrm ls

// 修改源
nrm use cnpm 

webpack可以进行零配置

webpack默认支持js、json,所以我们可以零配置打包js代码,默认会找src/index.js作为入口文件。

- src
  - index.js

// index.js
console.log('index start');

然后执行 npx webpack, 就在我们的dist/文件夹生成了一个打包后的main.js,此时默认采用mode为生产环境(production),会自动优化、压缩。

手动配置webpack

比如我们想更改mode为开发环境,希望打包后的代码不被压缩,我们可以手动配置webpack。默认配置文件的名字为 webpack.config.js或 webpackfile.js,其实扒开 webpack-cli 中就能找到这样一段代码,

// node_modules/webpack-cli/bin/config/config.yargs.js
defaultDescription: "webpack.config.js or webpackfile.js"

如果我们期望更改webpack启动配置文件的名称,可以通过 npx webpack --config someName.js来来指定文件。

新建文件 src/a.js,webpack.conf.my.js 并修改代码如下

// src/a.js
export default  {
  a: 1
}
// src/index.js
import obj from './a.js';

console.log(obj);
// webpack.conf.my.js
let path = require('path');

export default  {
  mode: 'development', // 更清晰的打包结果
  entry: './src/index.js', // 入口
  output: {
    filename: 'bundle.js', // 打包后的文件名
    path: path.resolve(__dirname, 'build') // 路径必须是个绝对路径
  }
}

package.json中增加build命令

{
    ...,

    "scripts": {
        "build": "npx webpack --config webpack.conf.my.js"
    },

    ...
}

分析打包后的文件

执行npm run build (如果需要额外参数 需要加一个 --),生成打包后的文件 build/build.js, 我们来分析下这个文件

CODE
 // webpack自执行启动函数 入参是一个对象 { './src/a.js': function, './src/index.js': function }
 (function(modules) { 
 	// 已缓存的模块 非首次加载的模块从这里面拿
 	var installedModules = {};

 	// 实现了一个require方法 接收一个模块id 模块id即是路径 类似 './src/a.js'
 	function __webpack_require__(moduleId) {

 		if (installedModules[moduleId]) {
      // 缓存中存在该模块,直接返回该模块
 			return installedModules[moduleId].exports;
 		}

 		// 声明一个新的 module 对象 并在缓存对象中缓存
 		var module = installedModules[moduleId] = {
 			i: moduleId, // 模块id
 			l: false, // 模块状态 是否加载成功
 			exports: {} // 模块导出
 		};

 		// 使用传入的 moduleId 对应的函数解析模块 
         //   + 把 module.exports 这个对象当成 this
         //   + 把 module 传入函数
         //     +「首次加载时,module 就是 { i: moduleId,l: false,  exports: {} }」
         //   + 把 module.exports 传入函数「首次加载时,就是 {}」
         //   + 把 __webpack_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;
 	}

 	// __webpack_require__ 上挂载全部传入的 modules 参数
 	__webpack_require__.m = modules;

 	// __webpack_require__ 上挂载 cache 的模块对象
 	__webpack_require__.c = installedModules;

 	// 给每个 exports 出的对象设置可枚举和可取值属性
 	__webpack_require__.d = function(exports, name, getter) {
 		if(!__webpack_require__.o(exports, name)) {
 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
 		}
 	};

 	// 为每个模块 exports 出对象设置 Symbol.toStringTag 
  // 也就是 Object.toString.call(exports) 返回的[object Module]
 	__webpack_require__.r = function(exports) {
 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
 		}
 		Object.defineProperty(exports, '__esModule', { value: true });
 	};

 	__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 函数默认值 不传的话为src/index.js 加载并return出加载到的模块exports的值
 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })
 ({ "./src/a.js":  (function(module, exports) {
    eval("module.exports = {\n  a: 1\n}\n\n//# sourceURL=webpack:///./src/a.js?");
  }),
  "./src/index.js": (function(module, exports, __webpack_require__) {
    // 注意这里 obj接收了 __webpack_require__的返回值,也就是对应模块的exports,所以是{ a: 1 },然后打印obj 得出结果
    eval("var obj  = __webpack_require__(/*! ./a.js */ \"./src/a.js\");\n\nconsole.log(obj);\n\n//# sourceURL=webpack:///./src/index.js?");
  })
});
  1. webpack启动函数是一个自执行函数,接收参数为{ 模块路径1: function, 模块路径2: function }这样一个对象,内部实现了自己require方法,先分析的入口文件问src/index.js,代码中遇到模块加载,则继续调用__webpack_require__方法加载模块。
  2. 加载过的文件会被缓存,第二次加载直接从内存中的缓存对象中拿。
  3. 任何打包进build.js中的模块,都有三个属性,{ 模块id,模块加载状态,模块exports }, 入口文件的导入的其实是其他模块的module.exports。

配置环境变量

  • webpack 中的 mode 字段,会在浏览器环境设置 process.env.NODE_ENV,但是 node 环境无法通过 process.env.NODE_ENV 访问(也可以启动命令通过 --mode 指定,优先级更高)。
  • 启动命令增加 --env,会给 webpack 配置传入参数,webpack.config.js 需要导出一个函数,第一个参数为 env,第二个参数为 { env },如果传入 --env=development,那么 env.development = true,通常用于区分环境,加载不同的 webpack 配置。
  • 在 node 环境设置变量怎么办呢?其实在 mac 中可以通过 export,在 window 下用 set,而兼容两者则需要使用 cross-env 设置~
"build": "export NODE_ENV=production && webpack"
"build": "set NODE_ENV=production && webpack"
"build": "cross-env NODE_ENV=production webpack"  // 注意不需要 && 符号啦
  • 还有一种方法,我们可以写一个 .env 文件,利用 dotenv 来实现 process.env 属性的读写~
// 安装 dotenv 包
yarn add dotenv
// 解析 .env 文件
require('dotenv').config({ path: 'env' });

热启动 webpack-dev-server

内部通过express来实现的这样一个静态服务,注意这样会新启一个服务,如果想复用现有的 express 服务实例,可以使用 webpack-dev-middleware,其实 webpack-dev-server 内部也使用的 webpack-dev-middleware,手动创建了个 express 实例传进去了而已~

yarn add webpack-dev-server -D

webpack-dev-server把打包文件写在内存中,所以不会在我们build文件夹中体现,可以通过npx webpack-dev-server来启动一个服务,当然更多我们会用到一些配置,webpack配置文件增加以下配置项

  devServer: { // webpack-dev-server配置
    port: 3000, // 启动端口
    progress: true, // 进度条
    publicPath: '/', // 静态文件目录
    contentBase: './build', // 以 build 目录作为额外的静态服务启动的文件
    compress: true, // 是否启用压缩
    open: true, // 自动打开浏览器
  }

build文件加新增文件index.html

<!DOCTYPE html>
    ...
    <script src="./bundle.js"></script>
</html>

配置启动命令

// package.json

"dev": "webpack-dev-server --config webpack.conf.my.js"

执行npm run dev,可以打开一个页面,并输出我们的结果。

html-webpack-plugin

我们并不希望在build文件夹中手动添加index.html,并手动引入资源,我们希望我们项目真正的html模板被打包,这时候,可以使用 html-webpack-plugin

yarn add html-webpack-plugin -D

src目录下新建index.html, 不用引入任何文件~

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>webpack4 学习</title>
</head>
<body>
  
</body>
</html>

webpack配置中增加plugin配置

  plugins: [ // 是一个数组 放着所有的webpack插件的实例
    new HtmlWebpackPlugin({
      template: './src/index.html', // 模板路径
      filename: 'index.html', // 打包后的文件名
      minify: {
        removeAttributeQuotes: true, // 删除html中的属性双引号
        // collapseWhitespace: true // 压缩成一行
      },
      hash: true // 引入资源路径后的hash值 比如./build?xxssss
    })
  ]

打包结果

<!DOCTYPE html>
    ...
    <script src=bundle.5cd64a02.js?5cd64a021942ccddb94f></script></body>
</html>

css-loader, style-loader

loader的作用就是对我们的源代码进行转换,使得原本 webpack 不认识的模块被正确解析和返回,要引入 css,需要正确的 loader将它转换成真正的 module。比如常用的css-loader,style-loader,需要注意它们的区别:

  • css-loader 是用来解析 @import 和 url 的,并将css包装成一个模块,模块导出的是base.css的内容,然后webpack将@import替换成__webpack_require__,去加载这个模块
  • style-loader 是把生成的css插入header标签中
  • 可以use多个loader,方法是把use对应的value配置成数组
  • loader是有顺序的,默认是从右向左执行,从下到上执行,比如我需要先处理css文件,然后把css内容插入到html的header标签的最底部,所以我需要把css-loader写后面, style-loader写在前面。

比如 index.css中引用 base.css,此时会打包出一个

 // ./node_modules/css-loader/dist/cjs.js!./base.css
 __webpack_exports__[\"default\"] = (___CSS_LOADER_EXPORT___);

 并且在打包出的index.css文件中把@import替换成了

 __webpack_require__("./node_modules/css-loader/dist/cjs.js!./base.css\");

配置style-loader, css-loader

  module: { // 模块
    rules: [ // 配置规则
        { 
            test: /\.css$/, 
            use: [
                { 
                    'style-loader',
                    options: {
                        // 插入到head标签顶部 这里略 
                        insert: function insertAtTop() {}
                    } 
                }, 
                {
                  loader: 'css-loader', // url import 进行处理
                  options: {
                    modules: {
                      mode: 'local',
                      localIndentName: "[path][name]__[local]--[hash:base64:5]" // 指定 css module 生成规则
                    },
                  },
                },
            ]
        }
    ]
  }

配置less-loader

less-loader 能帮我们处理less文件,把less文件转换成css文件,所以它应该在css-loader之前执行,根据单模块从右向左的执行顺序,less-loader应该配置在最右边。注意,less-loader会调用 less来进行语法转换,所以我们需要安装 less和 less-loader两个包。

yarn add less less-loader -D

更改配置

    module: { // 模块
        rules: [ // 配置规则
            { 
                test: /\.less$/, 
                use: [
                    'style-loader', 
                    'css-loader',
                    'less-loader'
                ]
            }
        ]
    }

mini-css-extract-plugin 抽离css文件

现在所有的css都放在style标签中了(style-loader干的),能不能把他提取成一个文件,通过link标签去引用呢,ok,mini-css-extract-plugin 就是做这样一个事儿,它的loader也取代了style-loader,因为我们不再需要style标签引入css了。

yarn add mini-css-extract-plugin -D

改下配置文件

// 如果需要抽离成多个css 可以多次引用 不同的变量名即可
let MiniCssExtractPlugin = require('mini-css-extract-plugin'); 

// plgin中使用
module.exports = {
    plugins: [
        new MiniCssExtractPlugin({ // 抽离css
            filename: '[name].css', // 抽离出的文件名
        })
    ],
    module: { // 模块
        rules: [ // 配置规则
            { 
                test: /\.less$/, 
                use: [
                    MiniCssExtractPlugin.loader, // 把css抽离出 并link标签引入 
                    'css-loader',
                    'less-loader'
                ]
            }
        ]
    }
}

看下打包后的文件

<!DOCTYPE html>
<html lang=en>
    <head>
    ...
    <link href=main.css?acd7a28111ac8e884862 rel=stylesheet></head>
    <body>
    <div id=study> 
        学习使我快乐
    </div>
    <script src=bundle.js?acd7a28111ac8e884862></script></body>
</html>

postcss-loader, autoprefixer 添加css前缀

我们使用autoprefixer来给css文件添加加各类浏览器兼容的前缀,以 Can I Use 上的 浏览器支持数据 为基础,自动处理兼容性问题,它需要搭配postcss-loader(后处理器)使用。

yarn add postcss-loader autoprefixer  -D

修改webpack配置文件

  module: { // 模块
    rules: [ // 配置规则
        { 
            test: /\.less$/, 
            use: [
                MiniCssExtractPlugin.loader, // 返回js,把css抽离出 并link标签引入 
                'css-loader',
                'postcss-loader', // 在解析css为模块和解析css内@import之前使用
                'less-loader'
            ]
        }
    ]
  }

根目录添加postcss.config.js 或者直接写进postcss-loader配置中

// postcss.config.js
module.exports = {
  plugins: [require('autoprefixer')]
}

最后一步: 在package.json中增加配置,也可以根目录创建.browserslistrc文件

  "browserslist": [
    "last 2 versions",
    "> 1%",
    "iOS 7",
    "last 3 iOS versions"
  ]

更:webpack5 可以使用 postcss-loader 配合 postcss-preset-env

let postCSSPresetEnv = require('postcss-preset-env');
module.exports = {
    plugins:[
        postCSSPresetEnv({ // 预设
            browsers:'last 10 version'
        })
    ]
}

uglifyjs-webpack-plugin, optimize-css-assetts-webpack-plugin (js,css压缩)

我们知道,当 mode为 production时,js会自动压缩,那 css不能压缩怎么办呢。

抽离css后,需要借助 optimize-css-assetts-webpack-plugin 帮我们压缩css,需要配置optimization.minimizer,然而配置之后,js又不能自动压缩了, 所以我们一般配合 uglify完成 css和 js的压缩任务。

let OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 
let UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin'); // 压缩js 配置css压缩后 js自动压缩失效 需要用此插件进行压缩

module.exports = {
    mode: 'production',
    optimization: [ // 优化项 只在生产环境生效
        new UglifyjsWebpackPlugin({ // 压缩js
            cache: true, // 是否使用缓存
            parallel: true, // 是否是并发压缩js的
            sourceMap: true // 压缩完后 源码映射 为了更好调试
        }),
        new OptimizeCssAssetsPlugin(); // 压缩 css
    ]
}

webpack5 我们一般使用 optimize-css-assets-webpack-plugin 结合 terser-webpack-plugin 做代码的压缩。

babel-loader,@babel/core,@babel/preset-env (es6 转成 es5)

如果我们在a.js中加了一个箭头函数,然后在入口文件引入a.js,却发现打包后的 bundle.js里面还是箭头函数(webpack预设的js模块解析并没有对js进行语法转换,我们需要添加自己的loader去干这件事),这在一些不支持es6语法的浏览器中会报错的,为了解决这个问题,我们需要把高版本的 js按照es5标准进行转换。

yarn add babel-loader @babel/core @babel/preset-env -D

babel-loader: 识别 js 文件。 @babel/core: 生成 AST,调用transform方法去转换 js源代码。 @babel/preset-env:转化规则,把标准的语法转换为低级的语法。

webpack 增加 babel-loader 配置

    module: { // 模块
        rules: [ // 配置规则
            { 
                test: /\.js$/, 
                use: [{
                    loader: 'babel-loader',
                    options: { // 用 babel-loader 把 es6转成 es5
                        presets: [ // 预设插件库
                            '@babel/preset-env'
                        ]
                    }
                }]
            }
        ]
    }

更快的 swc-loader

SWC 是一个类似于 Babel 的代码转义器,它的主要功能就是把 ES2015 或更高版本的 JS 代码转换为老浏览器能够使用的 ES5 或更低版本的 JS 代码。SWC 是使用 Rust 语言编写的,相比 Babel 来说,速度要更快。按照官网的说法 SWC 的速度要比 Babel 快 20 倍。

npm install --save-dev @swc/core swc-loader core-js
  module: {
    rules: [
      //  SWC 的 Loder 配置
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'swc-loader',
        },
      },
    ],
  }

@babel/plugin-syntax-class-properties 解析class类提案语法

如果我们在 a.js中添加了以下这段豪横无比的代码,webpack会无情报错的。

class Person {
  a = 1;
}

这时候,@babel/plugin-syntax-class-properties懂你

yarn add @babel/plugin-syntax-class-properties -D

更改webpack配置

  module: { // 模块
    rules: [ // 配置规则
        {
          test: /\.js$/,
          use: [{
            loader: 'babel-loader',
            options: { // 用 babel-loader 把 es6转成 es5
                presets: [ // 预设插件库 是大的插件的集合
                    '@babel/preset-env'
                ],
                plugins:  [ // 配置一个个的插件
                    '@babel/plugin-proposal-class-properties'
                ]
            }
          }]
        }
    ]
  }

@babel/plugin-proposal-decorators 解析类装饰器

@log
class Person {
  a = 1
}

function log(target) {
  console.log(target);
}

let ys = new Person();

我们给class类加一个修饰器,build的时候还是会报错的,因为修饰器提案想要进行语法转换也需要一个包,@babel/plugin-proposal-decorators

yarn add @babel/plugin-proposal-decorators -D

修改 babel-loader配置 此处参考babel官网提供的用法 babel-plugin-proposal-decorators

module: { // 模块
    rules: [ // 配置规则
        {
            test: /\.js$/,
            use: [{
                loader: 'babel-loader',
                options: { // 用 babel-loader 把 es6转成 es5
                    presets: [ // 预设插件库 是大的插件的集合
                    '@babel/preset-env'
                    ],
                    plugins:  [ // 配置一个个的插件
                        ["@babel/plugin-proposal-decorators", { "legacy": true }],
                        ["@babel/plugin-proposal-class-properties", { "loose" : true }]
                    ]
                }
            }]
        }
    ]
}

@babel/plugin-transform-runtime @babel/runtime 注入运行时辅助代码

此时我们再给入口index.js增加

// index.js
class Person2 {

}

打包后我们看到,bundle.js里面声明了两次 _classCallCheck 方法,是否可以公用呢?

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\");

我们在index.js文件中继续添加

function* gen() {
  yield 1;
}

console.log(gen().next());

然后npm run dev启动服务,打开页面,报错如下

Uncaught ReferenceError: regeneratorRuntime is not defined

wtf? 这个方法是哪里来的? 其实 Generator 或者更高级的 Promise 语法进行转化的时候,使用了 regeneratorRuntime 辅助方法却并没有加上这个辅助方法,需要增强编译,我们可以是使用 @babel/plugin-transform-runtime,这是一个代码运行时的包,会在代码运行时根据需要注入一些辅助性的代码。

@babel/plugin-transform-runtime是我们开发时候需要用的包,但是上线打包的话,我们也需要带上这些补丁代码,babel为我们提供了 @babel/runtime 去处理线上代码的打包。

yarn add @babel/plugin-transform-runtime -D
yarn add @babel/runtime   // 生产环境需要用的 就不加-D

webpack增加配置,注意,这步操作要排除 node_modules 中的 js,仅包含 src目录下的 js 代码 这时候可以看到,打包出的源码中自动引入了@babel/runtime下的包啦

/***/ "./node_modules/@babel/runtime/helpers/classCallCheck.js": 'xxx'

/***/ "./node_modules/@babel/runtime/helpers/classCallCheck.js": 'xxx'

注,在 webpack5 中,我们使用 babel-plugin-transform-runtime 来完成这个功能。

高级 api 不好使? babel-polyfill 垫一下

  • Babel默认只转换新的javascript语法,而不转换新的API,比如 Iterator, Generator, Set, Maps, Proxy, Reflect,Symbol,Promise 等全局对象。以及一些在全局对象上的方法(比如 Object.assign)都不会转码。
  • 比如说,ES6在Array对象上新增了Array.form方法,Babel就不会转码这个方法,如果想让这个方法运行,必须使用 babel-polyfill来转换等
  • babel-polyfill 它是通过向全局对象和内置对象的prototype上添加方法来实现的。比如运行环境中不支持Array.prototype.find方法,引入polyfill, 我们就可以使用es6方法来编写了,但是缺点就是会造成全局空间污染
  • @babel/preset-env 为每一个环境的预设,@babel/preset-env 默认支持语法转化,需要开启useBuiltIns配置才能转化API和实例方法,useBuiltIns 选项可选值包括:"usage" | "entry" | false, 默认为 false,表示不对 polyfills 处理,这个配置是引入 polyfills 的关键。

使用 babel-polyfill

npm i @babel/polyfill

useBuiltIns": false 此时不对 polyfill 做操作。如果引入 @babel/polyfill,则无视配置的浏览器兼容,引入所有的 polyfill,86.4kb。

import '@babel/polyfill';
{
  test: /\.jsx?$/,
  exclude: /node_modules/,
  use: {
      loader: 'babel-loader',
      options: {
        presets: [[
          "@babel/preset-env", 
          {
+           useBuiltIns: false,
          }
        ], 
        "@babel/preset-react"],
        plugins: [
            ["@babel/plugin-proposal-decorators", { legacy: true }],
            ["@babel/plugin-proposal-class-properties", { loose: true }]
        ]
      }

  }
}

"useBuiltIns": "entry" 含义

  • 在项目入口引入一次(多次引入会报错)
  • "useBuiltIns": "entry" 根据配置的浏览器兼容,引入浏览器不兼容的 polyfill。需要在入口文件手动添加 import '@babel/polyfill',会自动根据 browserslist 替换成浏览器不兼容的所有 polyfill
  • 这里需要指定 core-js 的版本, 如果 "corejs": 3, 则 import '@babel/polyfill' 需要改成 import 'core-js/stable';import 'regenerator-runtime/runtime';

corejs默认是2,配置2的话需要单独安装core-js@3

npm i core-js@3
import 'core-js/stable';
import 'regenerator-runtime/runtime';
{
  test: /\.jsx?$/,
  exclude: /node_modules/,
  use: {
      loader: 'babel-loader',
      options: {
          presets: [["@babel/preset-env", {
+         useBuiltIns: 'entry',
+         corejs: { version: 2 }
          }], "@babel/preset-react"],
          plugins: [
              ["@babel/plugin-proposal-decorators", { legacy: true }],
              ["@babel/plugin-proposal-class-properties", { loose: true }]
          ]
      }

  }
},
{
    "browserslist": {
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ],
    "production": [
      ">1%"
    ]
  },
}

"useBuiltIns": "usage" 含义

  • "useBuiltIns": "usage",usage 会根据配置的浏览器兼容,以及你代码中用到的 API 来进行 polyfill,实现了按需添加
  • 当设置为 usage 时,polyfills 会自动按需添加,不再需要手工引入 @babel/polyfill
  • usage 的行为类似 babel-transform-runtime,不会造成全局污染,因此也会不会对类似 Array.prototype.includes() 进行 polyfill
import '@babel/polyfill';
console.log(Array.from([]));
{
    test: /\.jsx?$/,
    exclude: /node_modules/,
    use: {
        loader: 'babel-loader',
        options: {
            presets: [["@babel/preset-env", {
+               useBuiltIns: 'usage',
+               corejs: { version: 3 }
            }], "@babel/preset-react"],
            plugins: [
                ["@babel/plugin-proposal-decorators", { legacy: true }],
                ["@babel/plugin-proposal-class-properties", { loose: true }]
            ]
        }
    }
},

最佳实践

babel-runtime 适合在组件和类库项目中使用,而 babel-polyfill 适合在业务项目中使用。

eslint 代码校验

手动配置

// eslint-loader 会调用 babel-eslint 转义高版本语法的代码
// 然后调用 eslint 进行代码检查
cnpm install eslint eslint-loader babel-eslint -D 

修改 webpack.config.js

module: {
  rules: [
    {
      test: /\.js$/, // 如果加载的模块是以 .js 结尾的
      loader: 'eslint-loader', // 进行代码风格检查
      enforce: 'pre', // pre loader
      options: { fix: true }, // 如果发现不合要求,会自动修复
      exclude: /node_modules/ 
    },
    // ...
  ]
}

增加 .eslintrc.js 文件

module.exports = {
  root: true, // 配置文件可以有继承关系的,这里是根配置
  parser: 'babel-eslint', // 把源代码解析为 AST 语法树的工具
  parserOptions: {
    sourceType: 'module',
    ecmaVersion: 2015
  },
  // 指定脚本的运行环境
  env: {
    browser: true
  },
  rules: {
    indent: 'off', // 缩进的风格
    quotes: 'off', // 引号的类型
    'no-console': 'error', // 不能出现 console
  }
}

此时再打包,如果我们入口文件中有 console.log,就会有警告啦,但是我们手动配置了三个规则,这显然是不够的,而全写的话又太多了,我们得考虑继承自一个比较完善的配置,然后做二次修改。

使用 eslint-config-airbnb

eslint-config-airbnb 是目前比较好的业界实践,先来安装包(笔者这里测试代码使用了 react):

{
    "eslint": "^7.28.0",
    "eslint-loader": "^4.0.2",
    "babel-eslint": "^10.1.0",
    "eslint-config-airbnb": "^18.2.1",
    "eslint-plugin-jsx-a11y": "^6.4.1",
    "eslint-plugin-react": "^7.24.0",
    "eslint-plugin-react-hooks": "^4.2.0",
    "eslint-plugin-import": "^2.23.4"
}

修改.eslintrc.js 文件

module.exports = {
  // root: true, // 配置文件可以有继承关系的,这里是根配置
  extends: 'airbnb', // 是继承自 airbnb 的配置
  parser: 'babel-eslint', // 把源代码解析为 AST 语法树的工具
  // 指定脚本的运行环境
  env: {
    browser: true,
    node: true, // node 环境下也可以使用
  },
  rules: {
    'linebreak-style': 'off', // 换行符检查关闭
    indent: ['error', 2],
    'no-console': 'off',
  },
};

新建 .vscode/settings.json,用于约束编辑器行为(js 文件保存时自动修复错误)~

{
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ],
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true 
  }
}

vscode 安装 eslint 插件,用于不规范代码提示,这个就不再赘述了~

测试代码

import React from 'react';
import ReactDOM from 'react-dom';

const a =     1;
console.log(a, React);

ReactDOM.render('hello', document.getElementById('root'));

npm run dev 就可以看到 a = 1 处标红,ctrl + s 保存代码时自动修复。

expose-loader在 window 挂载变量

很多第三方模块依赖jquery,而且依赖的是window.jquery,也就是全局的 jquery。

yarn add jquery

比如我们在a.js中增加如下代码

// a.js
import $ from 'jquery';

console.log(window.$, $, 'jquery'); // undefined fn jquery

我们看到打包后的文件里是有jquery的, jquery被包成一个闭包去执行,暴露出去,但是 window 上没有。如果有插件依赖了 window.jquery怎么办呢,别急,expose-loader可以暴露全局变量,注意,它是一个内联loader

yarn add expose-loader -D

语法如下

// expose-loader?exposes[]=暴露到全局的别名!引用包名
import $ from 'expose-loader?exposes[]=$!jquery';

console.log(window.$, $, 'jquery'); // fn fn jquery

当然也可以不用内联loader,配到webpack文件里去,参考官方写法。

module: {
    rules: [
        {
            test: require.resolve('jquery'),  // 只想找路径,而不想加载并执行
            loader: 'expose-loader',
            options: {
                exposes: ['$']
            }
        }
    ]
  }

但是我们发现,这样不是很友好,能不能直接用,而不需要每个模块都去去 import 呢?

webpack.ProvidePlugin 声明全局包

可以使用webpack.ProvidePlugin去声明全局变量

let webpack = require('webpack');

module.exports = {
    plugins: [
        // 把包引为全局变量,需要注意的是,这种全局变量并不会挂到 window 上
        new webpack.ProvidePlugin({ 
            $: 'jquery' // 引入 node_modules/jquery 声明为 $, 每个模块中都注入,而不是挂在window
        })
    ]
}
// index.js
consolle.log($); // fn

externals 忽略不需要被打包的模块(外部引入,模块内二次引入)

如果我在index.html通过 cdn 引入了 jquery,此时 window.jquery 即为 jquery,但是呢,我很任性,不希望用的时候写那么长的变量名,于是我做了以下操作

import $ from 'jquery';

这时一看打包文件,引入了双份的 jquery,我 emm..

我又比较有洁癖,只想引入一个包,怎么办呢? webpack提供了 externals 属性,它可以让我们打包时忽略已经外部引用的包。

externals: {
    jquery: '$'
}

其实它内部打包的时候,会依次遍历 externals 内配置,比如 jquery,如果配置过,那么 jquery 包就会变成 module.export = window.jquery 啦。

file-loader 和更强大的 url-loader 进行图片处理

图片有几种引入方式

  1. 在 js 中创建图片引入
  2. css background 引入 file-loader 用来解析图片,file-loader 默认会在内部生成一张带hash的图片,到 build 目录下,并且把生成的图片路径返回回来。
yarn add file-loader -D

js中引入图片

新建img.js,并在 index.js 中引入它。

import logo from '../logo.png'; // 注意 返回的是一个图片地址
  
let image = new Image();
image.src = logo; // 思考,如果我们这里直接访问一个图片呢,比如 '../logo.png' 它会被打包么?

document.body.append(image);

增加loader规则

module: { // 模块
    rules: [ // 配置规则
        {
            test: /\.(png|jpg|gif)$/,
            use: 'file-loader'
        },
    ],
}

ok, 大功告成

css中引入图片

// index.less

body {
  background: url(./logo.png) no-repeat;
}

因为 css-loader 默认会把 css 中引入的图片,转换成一个外部模块,比如以上代码,实际上被 css-loader 转换成以下代码,所以他也会打包。

body {
  background: url(require('./logo.png')) no-repeat;
}

假设我们有一个场景,如果图片小于 5k,就把它转成 base64(不会发请求,但是大小会比源文件大三分之一),否则用 file-loader产生真实的图片,防止过多的静态资源请求,这时候我们可以使用 url-loader。

yarn add url-loader -D

替换file-loader

module: { // 模块
    rules: [ // 配置规则
        {
            test: /\.(png|jpg|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    limit: 5 * 1024, // 5k以下转成base64 以上转成真正的图片
                }
            }
        }
    ]
}

ok,重新打包后,可以看到小图片被转成base64了

<img src="data:image/png;base64,/9j/4AAQSkZJRgABAQAASABIAAD"/>

但是这样会导致打包后的 js 文件变大,每次都需要重新打包,而在 webpack5 中,只要文件没有改变,就不会重新打包哦,这是 webpack5 性能提高 100% 的原因,就是靠硬盘缓存。

文件按类型打包到各自目录

图片分类 图片分类很简单,只需要把 url-loader 配置中加个 outputPath 即可,url-loader 会自动帮我们处理好图片的引入路径哦。

module: { // 模块
    rules: [ // 配置规则
        {
            test: /\.(png|jpg|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    limit: 5 * 1024, // 5k以下转成base64 以上转成真正的图片
                    outputPath: 'img/'
                }
            }
        }
    ]
}

css分类 css分类只需要更改 MiniCssExtractPlugin 输出的 filename 即可。

  plugins: [ // 是一个数组 放着所有的webpack插件的实例
    new MiniCssExtractPlugin({ // 抽离css
      filename: 'css/main.css', // 抽离出的文件名
    })
  ]

这时候 build 目录会出现 css/index.css 文件引入的背景图片路径为 img/xxx.png,解决办法是给 MiniCssExtractPlugin.loader 增加 publicPath

{ 
    test: /\.less$/, 
    use: [
        {   // 把css抽离出 并link标签引入 
            loader: MiniCssExtractPlugin.loader,
            options:{
                publicPath: '../'
            }
        }, 
        'css-loader',
        'postcss-loader',
        'less-loader'
    ]
}

ok,齐活儿。

sourcemap

我们在解析 js 中,会把一些高级语法转换成低级语法,甚至生产环境伴随着代码,比如下面一行代码报错,我们很难追溯到报错的地方。

// index.js

class Log {
    constructor() {
        console.lo('报错了');
    }
}

let log = new Log();

我们把 mode 改成 production,执行 npm run dev去启动服务。 页面报错了,错误出现在 bundle.js 第一行。

Uncaught TypeError: console.lo is not a function
    at new t (bundle.js?03a176d0736bff01997d:1)

点进去一看,都是压缩过的代码(这里截取一部分),实际情况更复杂,更难调试。

log(s().next()),"sssss".includes("s");new function t(){o()(this,t),console.lo("报错了")}}.call(this,a(159))}

是不是应该有一个映射文件,我点进去看到的应该是源码。

module.exports = {
    // 1) 源码映射 会单独生成一个 sourcemap文件 出错了会标识当前报错的列和行
    devtool: 'source-map', // 增加映射文件 可以帮我们调试源代码

    // 2) 'eval-source-map' 不会单独打包出 sourcemap 文件(集成到打包后的文件中),也可以显示行和列,并且显示源码
    // devtool: 'eval-source-map',

    // 3) 'cheap-module-source-map' 不会产生列,单独生成一个 sourcemap 文件
    // devtool: 'eval-source-map',

    // 4) 'cheap-module-eval-source-map' 不会产生列,不会产生文件,集成到打包后的文件中
    // devtool: 'cheap-module-eval-source-map',

这时候打包,页面能看到报错列和行

index.js:28 Uncaught TypeError: console.lo is not a function
    at new t (index.js:28)

点进去,可以看到源码

// ...
// 测试 source map
class Log {
    constructor() {
        console.lo('报错了');
    }
}
// ...

关键字

这里多提一嘴,看似 source-map 的种类很多,其实只有五个关键字 eval,source-map,cheap,module 和 inline 任意组合,但是有顺序要求(以下基于 webpack5 测试)。

关键字含义
eval使用 eval 包裹模块代码(打包后的源码,通过 eval 包裹字符串执行,webpack5 中直接通过字符串比对,方便缓存)
source-map产生 .map 文件,包含行、列和 loader 的映射
cheap不包含列信息也不包含 loader 的 sourcemap
module包含 loader 的 source(比如 jsx to js,babel 的 sourcemap),否则无法定义源文件
inline将 .map 作为 DataURI 嵌入,不单独生成 .map 文件

怎么理解不包含 loader 的 soucemap 呢?

那么 cheap-module 跟 source-map 有什么区别呢,它们的区别在于,source-map 具有行和列的信息,而 cheap-module 不包含列,只有行的信息。

组合规则

[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

  • source-map 单独在外部生成完整的 sourcemap 文件,并且在目标文件里建立关联,能提示错误代码的准确原始位置
  • inline-source-map 以 base64 格式内联在打包后的文件中,内联构建速度更快,也能提示错误代码的准确原始位置
  • hidden-source-map 会在外部生成 sourcemap 文件,但是在目标文件里没有建立关联,不能提示错误代码的准确原始位置
  • eval-source-map 会为每一个模块生成一个单独的 sourcemap 文件进行内联,并使用 eval 执行
  • nosources-source-map 也会在外部生成 sourcemap 文件,能找到源始代码位置,但源代码内容为空
  • cheap-source-map 外部生成 sourcemap 文件,不包含列和 loader 的 map
  • cheap-module-source-map 外部生成 sourcemap 文件,不包含列的信息但包含 loader 的map

如何选 & 最佳实践

开发环境

  • 我们在开发环境对sourceMap的要求是:速度快,调试更友好
  • 要想速度快 推荐 eval-cheap-source-map
  • 如果想调试更友好 cheap-module-source-map
  • 折中的选择就是 eval-source-map

生产环境

  • 首先排除内联,因为一方面我们了隐藏源代码,另一方面要减少文件体积
  • 要想调试友好 sourcemap > cheap-source-map/cheap-module-source-map > hidden-source-map/nosources-sourcemap
  • 要想速度快 优先选择 cheap
  • 折中的选择就是 hidden-source-map

代码如何调试

测试代码如下

let a = 1;
let b = 2;
let c = 3;

debugger
console.log(a, b, c);

测试环境

我期望的肯定是,只有我自己能调试,别人调试不了,根目录下新建 maps 文件夹,用于存放打包后的 map 文件。

  • 禁用 webpack 自动的 sourcemap 方案,手动维护
  • 使用 filemanager-webpack-plugin 将打包后的 map 文件拆出来,并在源文件中插入映射标记代码(手动维护映射),映射地址是我本地的 .map 文件。
+ // 文件管理器插件
+ const FileManagerPlugin = require('filemanager-webpack-plugin'); 

module.exports = {
  entry: {
    main: './src/index.js',
  },
  mode: 'development',
+ devtool: false, // 不生成任何 sourceMap 信息,手动控制
  plugins: [ // 是一个数组 放着所有的webpack插件的实例
    new HtmlWebpackPlugin({
      template: './src/index.html', // 模板路径
      publicPath: './'
    }),
+   new webpack.SourceMapDevToolPlugin({
+     filename: '[file].map' , // main -> main.js.map
+     // url 就是 main.js.map
+     append: `\n//# sourceMappingURL=http://127.0.0.1:3000/[url]` 
+   }),
+   new FileManagerPlugin({
+     events: {
+       onEnd: {
+         copy: [
+           {
+             source: './dist/*.map',  // 将 dist 目录下的所有 .map 文件
+             destination: path.resolve('maps') // 都拷贝到 maps 文件夹里
+           }
+         ],
+         delete: ['./dist/*.map'] // 处理完删除 dist/*.map
+       }
+     }
+   })
  ]
}

此时进行 npm run build,可以看到,在文件末尾指定了映射路径,并且 maps 也多出了 .map 文件~

/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
var a = 1;
var b = 2;
var c = 3;
debugger;
console.log(a, b, c);
/******/ })()
;
//# sourceMappingURL=http://127.0.0.1:3000/main.js.map 

这样就能实现,测试环境下,打包目录中没有 .map 文件,真正映射到的地址是我本地端口的文件,也就是只有我能调试啦~

生产环境

线上我们需要使用 hidden-source-map,它能实现打包出 map 文件,但是在源文件中不做关联,需要我们手动映射。

- const FileManagerPlugin = require('filemanager-webpack-plugin'); 

module.exports = {
  entry: {
    main: './src/index.js',
  },
  mode: 'development',
+ devtool: 'hidden-source-map', // 打包出 map 文件,但是在源文件中不做关联
  plugins: [ // 是一个数组 放着所有的webpack插件的实例
    new HtmlWebpackPlugin({
      template: './src/index.html', // 模板路径
      publicPath: './'
    }),
-   new webpack.SourceMapDevToolPlugin({
-     filename: '[file].map' , // main -> main.js.map
-     // url 就是 main.js.map
-     append: `\n//# sourceMappingURL=http://127.0.0.1:3000/[url]` 
-   }),
-   new FileManagerPlugin({
-     events: {
-       onEnd: {
-         copy: [
-           {
-             source: './dist/*.map',  // 将 dist 目录下的所有 .map 文件
-             destination: path.resolve('maps') // 都拷贝到 maps 文件夹里
-           }
-         ],
-         delete: ['./dist/*.map'] // 处理完删除 dist/*.map
-       }
-     }
-   })
  ]
}

npm run build 中的 main.js 文件没有做关联~

/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
var a = 1;
var b = 2;
var c = 3;
debugger;
console.log(a, b, c);
/******/ })()
;

启动服务,访问页面,我们入口文件加了 debugger 哦,发现没有定位到源文件,因为我们没有做关联,此时可以右键 add source map 我们本地打包出的 sourcemap 来做映射,同样达到本地调试线上代码的目的。