Webpack 4 构建大型项目实践 / 处理图片、样式和字体

3,786 阅读6分钟

本文所用示例的仓库地址: gayhub

上文使用 HtmlWebpackPlugin 生成了一个 index.html 文件,并且插件自动把打包后的资源添加到 index.html 文件中,使我们可以打开 index.html 在浏览器看到 js 的执行效果。本节我们将用 Webpack Loaders 来处理工作中会用到的其他三种类型文件:图片、样式、字体。

从这一节开始,由于项目资源类型变得复杂,每一节的测试项目会放到 demo/ 下,不再是文档根目录。

Loaders

官网对 Loaders 的介绍很简单,只有简单三点:

  • 可以用 Loaders 打包任何 Javascript 之外的任何静态资源
  • 用 Node.js 编写一个 Loader 很简单
  • 激活 Loader 有两种方式:
    • 在引入语句中添加 loaderName! , 比如 import 'style-loader!css-loader?modules!./css/test.css'
    • rules 中配置,见下文

处理图片

file-loader

yarn add file-loader

通常情况下,我们只需要把图片复制到(打包)目标目录,此时我们使用 file-laoder

{
  test: /\.(png|jpe?g|gif|webp)$/,
  use: [
    {
      loader: 'file-loader',
      options: {
        // 文件命名
        name: '[name].[ext]',
        // 输出路径
        outputPath: 'imgs/'
      }
    }
  ]
}

执行 webpack 命令后,我们可以看到图片已经被打包到了 dist/imgs/ 目录下,并且命名未修改。

url-loader

yarn add url-loader

当我们的图片文件存在一些小标签时,如果每个小标签都独立请求,显然是会造成带宽浪费的,这不合规矩。这个时候我们需要有类似雪碧图一样的方案,把多个小图片整合为一个请求。url-loader 就可以做这样的整合,但和雪碧图不同的是,它是把大小小于限定值的图片转成 base64 编码放到打包后的代码中。

{
  test: /\.(png|jpe?g|gif|webp)$/,
  use: [
    {
      loader: 'url-loader',
      options: {
        // 文件命名
        name: '[name].[ext]',
        // 输出路径
        outputPath: 'imgs/',
        // 小于 10k 的图片转成 base64 编码
        limit: 10240
      }
    }
  ]
}

执行 webpack 命令后,我们可以看到只有 多啦A梦.jpg 图片被打包到了 dist/imgs/ 目录下,其他 5 个小图被打包成了一串 base64 码(在 dist/dist.js 中)。

  • 大于 10k 的图

    /***/ "./src/imgs/多啦A梦.jpg":
    /*!***************************!*\
      !*** ./src/imgs/多啦A梦.jpg ***!
      \***************************/
    /*! no static exports found */
    /***/ (function(module, exports, __webpack_require__) {
    
    eval("module.exports = __webpack_require__.p + \"imgs/多啦A梦.jpg\";\n\n//# sourceURL=webpack:///./src/imgs/%E5%A4%9A%E5%95%A6A%E6%A2%A6.jpg?");
    
    /***/ }),
    
  • 小于 10k 的图

    /***/ "./src/imgs/分享.png":
    /*!*************************!*\
      !*** ./src/imgs/分享.png ***!
      \*************************/
    /*! no static exports found */
    /***/ (function(module, exports) {
    
    eval("module.exports = \"data:image/png;base64,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\"\n\n//# sourceURL=webpack:///./src/imgs/%E5%88%86%E4%BA%AB.png?");
    /***/ })
    

处理样式

如果我们不使用 loader 来处理样式,执行 webpack 可以看到这样的提示 You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.

CSS

yarn add css-loader style-loader
  • css-loader 解析 CSS 文件
  • style-loader 把样式插入到 DOM 中

loader 从右往左、从下往上执行

{
    test: /\.css$/,
    use: [
        'style-loader',
        'css-loader'
    ]
}

此时执行 webpack 就能正确打包了,但并不会再 dist/ 下生成 .css 文件,而是把样式以 style 标签的形式插入到 index.htmlhead 标签中(在浏览器控制台查看,插入前样式文件在 JS 中)。

CSS 预处理器

由于我是看黄轶讲师的“高仿饿了么”课程后,才开始在项目中使用预处理器,所以会更喜欢 stylus ,本文的例子也使用 stylus 。

stylus-loader 默认项目中存在 stylus 依赖,所以安装 stylus-loader 时不要忘记安装 stylus

yarn add stylus stylus-loader css-loader style-loader
{
    test: /\.styl(us)?$/,
    use: [
        'style-loader',
        {
            loader: 'css-loader',
            options: {
                importLoaders: 1 // 在 css-loader 前执行的 loader 数量
            }
        },
        {
            loader: 'stylus-loader',
            options: {
                preferPathResolver: 'webpack' // 优先使用 webpack 用于路径解析,找不到再使用 stylus-loader 的路径解析
            }
        }
    ]
}

PostCSS

几年以前写 CSS 有个蛋疼的地方,某些属性我们需要为不同浏览器加上不同的前缀,比如 Firefox 的 -moz-transform 和 IE 的 -ms-transform ,但有了 PostCSS 后你只需要写无前缀的属性,PostCSS 会根据 Can i use 的数据为你的 CSS 属性补充上前缀。

yarn add postcss-loader -D
{
    test: /\.styl(us)?$/,
    use: [
        'style-loader',
        {
            loader: 'css-loader',
            options: {
                importLoaders: 2 // 在 css-loader 前执行的 loader 数量
            }
        },
        'postcss-loader',
        {
            loader: 'stylus-loader',
            options: {
                preferPathResolver: 'webpack' // 优先使用 webpack 用于路径解析,找不到再使用 stylus-loader 的路径解析
            }
        }
    ]
}

PostCSS 配置文件 postcss.config.js ,配置自动补全(需要 autoprefixer 插件)

yarn add autoprefixer -D

postcss.config.js

module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}

此时执行 webpack 命令,可以在浏览器中看到 transform rotate(180deg) 被翻译成 -webkit-transform: rotate(180deg); transform: rotate(180deg);

两个注意点:

  • postcss-loader 的顺序,在 css-loader 前一步执行
  • 可以在 Webpack 配置 JS 中配置 postcss-loader 的地方使用 options 来配置 PostCSS ,但更推荐在项目根目录添加 postcss.config.js 配置文件,别人能更容易知道你使用了 PostCSS。

从 JS 分离出样式文件

上文有提到 style-loader 把样式插入到 html 文件,这样做减少了请求数。但正常项目样式文件会占据不小的体积,要知道在插入 index.html 前我们的样式文件是储存在 JS 文件中的,所以 JS 文件会非常大,并且我们可能需要更清晰的生成物结构,所以也应当清楚如何把样式从 js 中分离出来 —— 使用 MiniCssExtractPlugin

yarn add mini-css-extract-plugin -D

style-loader 替换为 MiniCssExtractPlugin Loader

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  module: {
    rules: [
        {
            test: /\.styl(us)?$/,
            use: [
                {
                    loader: MiniCssExtractPlugin.loader,
                    options: {
                        publicPath: '../',
                        hmr: process.env.NODE_ENV === 'development',
                        reloadAll: true
                    },
                },
                {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 2 // 在 css-loader 前执行的 loader 数量
                    }
                },
                'postcss-loader',
                {
                    loader: 'stylus-loader',
                    options: {
                        preferPathResolver: 'webpack' // 优先使用 webpack 用于路径解析,找不到再使用 stylus-loader 的路径解析
                    }
                }
            ]
        }
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].css',
      chunkFilename: 'css/[name].css',
    }),
  ]
};

执行 webpack 命令后可以看到,在 dist/css 目录下生成了 main.css 文件,并且该文件在 index.html 中引入了。

处理字体文件

字体文件和图片类似,只需要拷贝并放到 dist/fonts/ 目录下,仍然使用 file-loader / url-loader

{
    test: /\.(woff2?|eot|ttf|otf|svg)(\?.*)?$/i,
    use: {
        loader: 'url-loader',
        options: {
            limit: 4096,
            name: '[name]_[hash:5].[ext]',
            outputPath: 'fonts/'
        }
    }
}

另外提一句,现在更流行使用 svg 来绘制图标,绘制路径存在 iconfong.js 中(阿里图标库下载举例),此时切记不要把 iconfong.js 放在 src/ 目录,因为打包 iconfont.js 毫无意义,只是增加打包时长罢了。应当放到不用打包的静态目录,比如 vue-cli 2 的 static/ 和 vue-cli 3 的 public/ ,并在 html 中引用他们,打包时使用 CopyWebpackPluginstatic / public 整个文件夹复制到 dist/

参考文档