webpack 快速入门 系列 —— 实战一

其他章节请看:

webpack 快速入门 系列

实战一

准备本篇的环境

虽然可以仅展示核心代码,但笔者认为在一个完整的环境中边看边做,举一反三,效果更佳。

这里的环境其实就是初步认识 webpack一文完整的示例,包含 webpack、devServer、处理css、生成 html。

项目结构如下:

webpack-example2        
  - src                 // 项目源码
    - a.css
    - b.js
    - c.js
    - index.html        // 页面模板
    - index.js          // 入口
  - package.json        // 存放了项目依赖的包
  - webpack.config.js   // webpack配置文件

src中的代码如下:

// a.css
body{color:blue;}

// b.js
import \'./c.js\'
console.log(\'moduleB\')
console.log(\'b2\')

// c.js
console.log(\'moduleC\')

// index.html
<!DOCTYPE html>
<html >
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=`, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <p>请查看控制台</p>
</body>
</html>

// index.js
import \'./b.js\'
import \'./a.css\'
console.log(\'moduleA\')

package.json:

{
  "name": "webpack-example2",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "css-loader": "^5.2.4",
    "html-webpack-plugin": "^4.5.2",
    "style-loader": "^2.0.0",
    "webpack": "^4.46.0",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.2"
  }
}

webpack.config.js:

const path = require(\'path\');
const HtmlWebpackPlugin = require(\'html-webpack-plugin\')
module.exports = {
  entry: \'./src/index.js\',
  output: {
    filename: \'main.js\',
    path: path.resolve(__dirname, \'dist\')
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"]
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
        template: \'src/index.html\'
    })
  ],
  mode: \'development\',
  devServer: {
    open: true,
    contentBase: path.join(__dirname, \'dist\'),
    compress: true,
    port: 9000,
  },
};

在 webpack-example2 目录下运行项目:

// 安装项目依赖的包
> npm i
// 启动服务
> npm run dev

启动服务器后,浏览器会自动打开页面,如果看到蓝色文字”请查看控制台“,说明环境已准备就绪。

打包样式

处理 css 和 less

less 是一种 css 预处理语言,在 webpack 中要处理 less 需要使用 less-loader,用于将 less 转为 css。

首先安装依赖,然后修改配置文件:

// 安装包。版本8安装失败,所以降了一个版本
> npm i -D less-loader@7

// webpack.config.js
// 增加对 less 文件处理的loader
rules: [
    // 需要保留,否则识别不了 css 文件
    {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"]
    },
    // +
    {
        test: /\.less$/i,
        loader: [
            // compiles Less to CSS
            "style-loader",
            "css-loader",
            "less-loader",
        ],
    },
],

然后增加 a.less 文件,在 index.js 中引入 a.less,重新启动服务进行测试:

// src/a.less
body{
    p{
        color:pink;
    }
}

// index.js
import \'./b.js\'
import \'./a.css\'
// +
import \'./a.less\'
console.log(\'moduleA\')

// 启动服务
> npm run dev 

在新开的页面中,看到粉色文字”请查看控制台“,说明 less 处理成功。

提取 css 成单独文件

通过浏览器我们发现现在 css 是嵌在页面内的,就像这样:

<head>
  ...
  <style>body{color:blue;}</style>
  <style>body p {
    color: pink;
  }
  </style>
</head>

通常我们会通过 link 来引入 css 文件,所以接下来就将 css 取成单独的文件。这里需要使用 mini-css-extract-plugin 这个包。

我们只需要安装依赖包,修改配置文件即可:

// 安装依赖包 
> npm i -D mini-css-extract-plugin@1
// 不在需要 style-loader,卸载
> npm r -D style-loader

// webpack.config.js
// +
const MiniCssExtractPlugin = require(\'mini-css-extract-plugin\');
module.exports = {
  ...
  module: {
    rules: [
      // 修改规则
      {
        test: /\.css$/i,
        // 将 style-loader 改为 MiniCssExtractPlugin.loader
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
      {
        test: /\.less$/i,
        loader: [
          // 将 style-loader 改为 MiniCssExtractPlugin.loader
          MiniCssExtractPlugin.loader,
          "css-loader",
          "less-loader",
        ],
      },
    ],
  },
  plugins: [
    // +
    new MiniCssExtractPlugin(),
    ...
  ],
  
};

启动服务(npm run dev),在打开的页面中可以看到 css 已经改为 link 的方式引入,就是这样:

// 从 style 改为 link 方式
<link href="main.css" rel="stylesheet">

// 通过网络查看 main.css 的内容是:
body{color:blue;}
body p {
  color: pink;
}

由于我们对 css 和 less 都使用了 MiniCssExtractPlugin.loader,所以 a.css 和 a.less 都被提取到 main.css 中。

Tip:如果通过npm run build打包,则可以看到 dist/main.css 文件。

使用 PostCSS

PostCSS - 使用JavaScript转换CSS的工具。

可以将 postcss 当作一个平台,下面我们通过 postcss 做两件事:

  • 增加代码可读性(或增加前缀)
:fullscreen {
}

// 转为

:-webkit-full-screen {
}
:-ms-fullscreen {
}
:fullscreen {
}
  • 立即使用明天的CSS
body {
  color: lch(53 105 40);
}

// 转为

body {
  color: rgb(250, 0, 4);
}

webpack 可以通过 postcss-loader 来使用 postcss。

由于 postcss 只是一个平台,具体功能需要通过插件来实现,这里我们使用 postcss-preset-env

postcss-preset-env 可以将现代CSS转换为大多数浏览器可以理解的内容,并根据目标浏览器或运行时环境确定所需的polyfill。而且它包含自动前缀。

首先安装相关依赖,并修改配置文件:

> npm i -D postcss-loader@4 postcss-preset-env@6

// webpack.config.js
// +
const postcssPresetEnv = require(\'postcss-preset-env\');
// +
const postcssLoader = { 
  loader: \'postcss-loader\', 
  options: {
    // postcss 只是个平台,具体功能需要使用插件
    // Set PostCSS options and plugins
    postcssOptions:{
      plugins:[
        // 配置插件 postcss-preset-env
        [
          "postcss-preset-env",
          {
            // 自动前缀。默认是true
            // autoprefixer: true,
            // 根据您所支持的浏览器来确定需要哪些polyfill。这里仅做演示
            browsers: \'ie >= 8, chrome > 10\',
            // stage 默认是 2
            // stage:2
          },
        ],
      ]
    }
  } 
}

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          MiniCssExtractPlugin.loader, 
          "css-loader",
          // + 放在css-loader后面
          postcssLoader
        ]
      },
      {
        test: /\.less$/i,
        loader: [
            MiniCssExtractPlugin.loader,
            "css-loader",
            // +
            postcssLoader,
            "less-loader",
        ],
      },
    ]
  },
};

接着修改 a.css 和 a.less,重新启动服务器:

// a.css
body{
    color: lch(53 105 40);
}

// a.less
body{
    p{
        transform: scale(1, 2);
    }
}

// 启动服务
> npm run dev

在新开的页面中,我们看到红色文字”请查看控制台“,而且文字纵向拉长了一倍。通过浏览器查看 main.css 源码如下:

body{
    color: rgb(250, 0, 4);
}
body p {
  -webkit-transform: scale(1, 2);
      -ms-transform: scale(1, 2);
          transform: scale(1, 2);
}

至此,增加前缀以及立即使用明天的CSS都已经完成。

Tip:stage(阶段)可以是0(实验)到4(稳定),默认是2,如果我们改为3或4,重新打包,lch(53 105 40);则不会转为 rgb(250, 0, 4);将plugins换成下面的写法效果相同。

plugins:[
  postcssPresetEnv({
    browsers: \'ie >= 8, chrome > 10\',
  })
]

postcss-preset-env 支持任何标准的 browserslist 配置,可以是 .browserslistrc 文件,package.json 中的browserslist 键或 browserslist 环境变量。

如果将 browsers: \'ie >= 8, chrome > 10\', 注释,browsers 将使用默认的 browserslist 查询(即> 0.5%, last 2 versions, Firefox ESR, not dead),重新构建,则不会添加前缀。

如果不想在 browsers 中写,在 package.json 中的 browserslist 中配置也是可以的:

// package.json
{
  ...
  "browserslist": [
      "ie >= 8",
      "chrome > 10"
  ]
}

:package.json 不能写注释,本文在 package.json 中的注释仅作说明。

如果觉得 package.json 写的内容太多,我们甚至可以将这部分提取到一个单独的文件中来写:

// .browserslistrc
// from github browserslist
# Browsers that we support

ie >= 8
chrome > 10

最后,如果我们针对开发环境和生成环境做不同的处理,比如开发环境支持 ie8+,而生产环境支持 chrom10+,我们可以这么写:

// .browserslistrc
# Browsers that we support

[production]
chrome > 10

[development]
ie >= 8

然后在配置文件中通过 process.env 来指定环境:

// +
// process.env属性返回一个包含用户环境的对象
process.env.NODE_ENV = \'development\' // or production

Browserslist将根据BROWSERSLIST_ENV或NODE_ENV变量选择,所以设置 process.env.BROWSERSLIST_ENV 也是可以的。

再次打包 npm run build,则只会针对 ie,生成的 main.css 内容如下:

body{
    color: rgb(250, 0, 4);
}
body p {
  -ms-transform: scale(1, 2);
      transform: scale(1, 2);
}

压缩 css

如果我们需要压缩 css 代码,可以使用 optimize-css-assets-webpack-plugin,用于优化或最小化 css。

首先安装依赖,然后修改配置:

> npm i -D optimize-css-assets-webpack-plugin@5

// webpack.config.js
// +
const OptimizeCssAssetsPlugin = require(\'optimize-css-assets-webpack-plugin\');

plugins: [
    new HtmlWebpackPlugin({
        template: \'src/index.html\'
    }),
    new MiniCssExtractPlugin(),
    // +
    new OptimizeCssAssetsPlugin()
  ],

重新打包,原来的 main.css 则变成一行,请看:

> npm run build

// main.css(优化前)
body{
    /* 注释 */
    color: rgb(250, 0, 4);
}
body p {
  -ms-transform: scale(1, 2);
      transform: scale(1, 2);
}

// main.css(优化后)
body{color:#fa0004}body p{-ms-transform:scaleY(2);transform:scaleY(2)}

优化后,css 变成了一行,注释也删除了。

打包图片

前端资源通常有图片,由于 webpack 只识别 javascript,所以需要 loader 来帮们识别图片。

我们使用 url-loader,能将图片转为 base64。

首先安装依赖,并修改配置文件:

> npm i -D url-loader@4

// webpack.config.js
module: {
  rules: [
    ...
    // +
    {
      test: /\.(png|jpg|gif)$/i,
      use: [
        {
          loader: \'url-loader\',
          options: {
            // 指定文件的最大大小(以字节为单位)
            limit: 1024*7,
          },
        },
      ],
    },
  ]
},

接着引入图片,启动服务:

// 引入图片。src/6.68kb.png

// a.less
body{
    p{
        transform: scale(1, 2);
    }
    .m-box{display:block;width:100px;height:100px;}
    .img-from-less{background:url(./6.68kb.png) no-repeat;background-size:100% 100%;}
}

// index.html
...
<body>
    <p>请查看控制台</p>
    <span class=\'m-box img-from-less\'></span>
</body>
...

// 启动服务
> npm run dev

Tip: 笔者的图片大小为 6.68kb,上面的 limit 只需要大于6.68kb即可

在新开的页面中,我们在”请查看控制台“文字下面看见了我们设置的图片。通过检查元素会发现这张图片是 base64。

body .img-from-less{
  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA... no-repeat;
  ...
}

如果将 limit: 1024*7 修改为 limit: 1024*6(也就是将 limit 设置的比图片的 size 更小),再次运行 npm run dev,会发现报错了。还会提示找不到 file-loader。这是因为这张图片(6.68kb.png)大于 1024*6,所以就不会被打包成 base64,所以需要 file-loader 来处理。

安装依赖包 npm i -D file-loader@6,再次启动服务器,页面上又看到我们的图片,而且这次不再是 base64,而是直接生成了一张图片。

body .img-from-less{
  background: url(26bd867dd65e26dbc77d1e151ffd36e0.png) no-repeat;
  ...
}

图片除了在 css 中使用,我们也会通过 img 元素引用,于是我们在 index.html 中新增 <img class=\'m-box\' src="./6.68kb.png" alt=""> 再次启动服务,在打开的浏览器页面中发现 img 引用的图片没生效,而且源码也没变化。

这里需要使用 html-loader 这个包,它能让每个被加载的属性(例如:<img src="image.png")能被引入(imported)。

安装依赖包,修改配置:

> npm i -D html-loader@1

// webpack.config.js
module: {
    rules: [
      ...
      // +
      {
        test: /\.html$/i,
        loader: \'html-loader\',
      },
    ]
},

再次启动服务,就能看到两张一样的图片了。img 的代码变为 <img class="m-box" src="26bd867dd65e26dbc77d1e151ffd36e0.png" alt="">

如果再次将 limit: 1024*6 修改为 limit: 1024*7,启动服务你会发现这两处图片都变为 base64。

打包 javascript

js 语法检查

有时我们希望团队成员写的 javascript 代码风格一致。

我们可以使用 eslint,它能查找并修复JavaScript代码中的问题;可以自定义 eslint,使其完全按照项目所需的方式工作。代码风格,笔者选用 airbnb,一个流行的 javascript 风格指南(此刻是第 6 名(topics javascript))。

在 webpack 中使用 eslint,需要使用 eslint-webpack-plugin( eslint-loader废弃了),而 eslint-webpack-plugin 依赖于 eslint

eslint-config-airbnb 默认导出包含我们所有的ESLint规则,包括ECMAScript 6+和React,而 我们不需要使用 react,所以使用 eslint-config-airbnb-base 即可。

首先安装依赖包,修改配置:

// 没有引入 eslint-plugin-import
> npm i -D eslint@7 eslint-webpack-plugin@2 eslint-config-airbnb-base@14 

// webpack.config.js
// +
const ESLintPlugin = require(\'eslint-webpack-plugin\');

module.exports = {
  // ...
  plugins: [
    new ESLintPlugin({
      // 将启用ESLint自动修复功能。此选项将更改源文件
      fix: true
    })
    
  ],
  // ...
};

// package.json
{
  // +
  "eslintConfig": {
    "extends": "airbnb-base"
  }
}

重新打包 npm run build,出现了一些警告和错误,核心信息如下:

WARNING in 

webpack-example2\src\index.js
  6:1  warning  Unexpected console statement  no-console

✖ 6 problems (2 errors, 4 warnings)

ERROR in 

webpack-example2\src\index.js
  1:8  error  Unexpected use of file extension "js" for "./b.js"  import/extensions

✖ 5 problems (2 errors, 3 warnings)

错误(import/extensions)是不希望使用 js 扩展名,将 ./b.js 改为 ./b 就好了,可参考issues:import/extensions

警告(no-console)是因为不能出现 console.log。可以通过配置将这个告警关闭:

// package.json
{
  "eslintConfig": {
    "extends": "airbnb-base",
    // +
    "rules": {
        "no-console": "off",
    }
  }
}

import/extensions修复,并将警告关闭,重新打包 npm run build则不会出现警告和错误。

Tip:打包后,源码也会自动修复,比如 src/index.js 中的 sum() 方法,a, 后面是多个空格,打包后会合并成一个空格:

function sum(a,    b) {
  return a + b;
}
// 需要调用 sum() 方法
// 否则报错:error  \'sum\' is defined but never used  no-unused-vars
console.log(sum(1, 100));

// 修复后

function sum(a, b) {
  return a + b;
}

如果在 js 文件中使用 window ,再次打包会报错,就像这样:

// index.js
// +
setTimeout(() => {
  window.location = \'https://www.baidu.com/\';
}, 1000);

// 打包
> npm run build
...
error  \'window\' is not defined  no-undef

可以在配置文件中指定环境来解决这个问题。就像这样:

// package.json
"eslintConfig": {
    // +
    "env": {
      "browser": true
    }
}

如果不想写到 package.json,也可以配置到单独的文件(.eslintrc.js)中:

// .eslintrc.js
module.exports = {
    "extends": "airbnb-base",
    "rules": {
        "no-console": "off"
    },
    "env": {
      "browser": true
    }
}

js 兼容性处理

我们想使用 es6 来编写代码,但有的浏览器支持的不够全面,所以我们会将 es6 转成 es5。

接着上面的例子进行,重写 index.js,放入一个箭头函数,再次打包,你会发现 webpack 不会对 es6 语法做处理,const 还是 const,而不是 var:

// src/index.js
const sum = (a, b) => (a + b);
console.log(sum(1, 10));

// sum 还是我们的箭头函数
eval("const sum = (a, b) => (a + b);\nconsole.log(sum(1, 10));\n\n\n//# sourceURL=webpack:///./src/index.js?");

Babel 是一个 JavaScript 编译器。通过它可以让我们使用下一代的 JavaScript 语法编程。

在 webpack 中要使用 babel 就得用 label-loader。用法(Usage)如下:

// 安装依赖包。没有使用 @babel/core
> npm i -D babel-loader@8 @babel/preset-env@7

// webpack.config.js
module: {
  rules: [
    // +
    {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: \'babel-loader\',
          options: {
            presets: [
              [\'@babel/preset-env\']
            ]
          }
        }
    }
  ]
}

重新打包,箭头函数就变成普通函数:

eval("var sum = function sum(a, b) {\n  return a + b;\n};\n\nconsole.log(sum(1, 10));\n\n//# sourceURL=webpack:///./src/index.js?");

如果我们在 js 中使用 Promise,重新打包后 Promise 还是 Promise,而且在不识别 Promise 语法的浏览器中(比如 ie11)运行会报错。

// index.js
Promise.resolve(\'aaron\').then((v) => {
  console.log(v);
});

// 重新打包,Promise 还是 Promise
eval("Promise.resolve(\'aaron\').then(function (v) {\n  console.log(v);\n});\n\n//# sourceURL=webpack:///./src/index.js?");

babel 官网提到,使用@babel/polyfill,就可以使用新的内置函数(例如Promise或WeakMap),静态方法(例如Array.from或Object.assign),实例方法(例如Array.prototype.includes)等等。所以这个 polyfill 是我们的解决方案。

但是 @babel/polyfill 废弃了。而 @babel/polyfill 包含 regenerator runtime 和 core-js。

core-js,包括适用于2021年前ECMAScript的polyfill,而且仅加载必需的功能。

在 useBuiltIns 参数中也提到:由于在7.4.0中已弃用@ babel/polyfill,因此我们建议直接添加core-js并通过corejs选项设置版本。

于是我们知道 core-js 能解决 Promise 这类问题。

如何使用 core-js ?我们先来介绍一下插件预设

babel 通过将插件(或预设)应用于配置文件来启用Babel的代码转换。比如插件列表中的es2015,这是一个集合,包含了箭头函数(arrow-functions)、类(classes)等插件;

而预设(presets)其实是多个插件(plugin)的集合。比如 @babel/preset-env 这种预设则包含了 es2015、es2016、es2017等最新的插件。

最后根据babel-preset-env中的介绍,我们将 core-js 应用上:

// 安装依赖
> npm i -D core-js@3.11

// webpack.config.js
{
  loader: \'babel-loader\',
  options: {
    presets: [
      [
        \'@babel/preset-env\', 
        // +
        {
          // 配置处理polyfill的方式
          useBuiltIns: "usage",
          // 版本与我们下载的版本保持一致
          corejs: { version: "3.11"},
          "targets": "> 0.25%, not dead"
        }
      ]
    ]
  }
}

重新打包,dist/main.js 的Size变成 109 Kib,而之前还不到 4kiB。

启动服务,在不支持 Promise 语法的浏览器中,比如 ie11,也能在控制台输入 aaron。

至此 javascript 的兼容处理完毕。

其他章节请看:

webpack 快速入门 系列