乐趣区

webpack4功能配置划分细化三

前言

之前一段时间工作原因把精力都放在小程序上, 趁现在有点空闲时间, 刚好官方文档也补充完整了, 我准备重温一下 webpack 之路了, 因为官方文档已经写得非常详细, 我会大量引用原文描述, 主要重点放在怎么从零构建 webpack4 代码上, 这不是一个系统的教程, 而是从零摸索一步步搭建起来的笔记, 所以前期可能 bug 会后续发现继续修复而不是修改文章.

基本已经可以使用的完整配置 webpack4_demo,

继续上回分解, 我们之前已经实现了资源处理, 配置环境分开, 引入 React 库和 babel 库, 图片优化和打包可视化, 这一章我们就将零散的文件进一步规格化配置

2018/12/26 上传, 代码同步到第四篇文章
2019/03/14 上传, 补充代码到第三篇文章

配置文件

我们在根目录单独新建文件夹config, 将所有 webpack 配置文件放进去, 然后改一下相对路径的引入, 接下来抽取出些配置文件单独一个模块管理.

alias.js

路径简化单独一个配置文件方便查找

const path = require('path');

// 创建 import 或 require 的别名,来确保模块引入变得更简单
module.exports = {"@": path.resolve(__dirname, "../src/"),
  IMG: path.resolve(__dirname, "../src/img"),
  STYLE: path.resolve(__dirname, "../src/style"),
  JS: path.resolve(__dirname, "../src/js"),
  ROUTER: path.resolve(__dirname, "../src/router"),
  PAGE: path.resolve(__dirname, "../src/page"),
  CMT: path.resolve(__dirname, "../src/component")
};

rules.js

规则处理单独一个模块, 实在太多了

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

module.exports = [
  {test: /\.(js|jsx)$/, // 匹配文件
    exclude: /node_modules/, // 过滤文件夹
    use: {loader: "babel-loader"}
  },
  {
    test: /\.s?css$/, // 匹配文件
    use: [
      {
        loader: MiniCssExtractPlugin.loader,
        options: {
          // you can specify a publicPath here
          // by default it use publicPath in webpackOptions.output
          publicPath: "../"
        }
      },
      // "style-loader", // 使用 <style> 将 css-loader 内部样式注入到我们的 HTML 页面
      "css-loader", // 加载.css 文件将其转换为 JS 模块
      "sass-loader" // 加载 SASS / SCSS 文件并将其编译为 CSS
    ]
  },
  {test: /\.(html)$/,
    use: {
      loader: "html-loader",
      options: {attrs: ["img:src", "img:data-src", "audio:src"],
        minimize: true
      }
    }
  },
  {test: /\.(png|svg|jpe?g|gif)$/i, // 图片处理
    use: [
      {
        loader: "url-loader",
        options: {name: "[name].[hash:5].[ext]",
          limit: 20 * 1024, // size <= 50kb
          outputPath: "img"
        }
      },
      {
        loader: "image-webpack-loader",
        options: {
          // Compress JPEG images
          mozjpeg: {
            progressive: true,
            quality: 65
          },
          // Compress PNG images
          optipng: {enabled: false},
          //  Compress PNG images
          pngquant: {
            quality: "65-90",
            speed: 4
          },
          // Compress GIF images
          gifsicle: {interlaced: false},
          // Compress JPG & PNG images into WEBP
          webp: {quality: 75}
        }
      }
    ]
  },
  {test: /\.(woff|woff2|eot|ttf|otf)$/, // 字体处理
    use: ["file-loader"]
  },
  {
    test: /\.xml$/, // 文件处理
    use: ["xml-loader"]
  }
];

webpack.common.js

现在改改路径和引入, 瞬间清爽很多, 有个地方需要注意的是现在配置文件和 dist 文件不在同一个层级, 默认是不允许删除层级之上, 我们需要开放权限
因为最近更新版本不支持以前写法, 所以替换一下

// 清除文件
new CleanWebpackPlugin({
      dangerouslyAllowCleanPatternsOutsideProject: true,
      cleanOnceBeforeBuildPatterns: ["../dist"],
      dry: true
}),
const path = require("path"),
  HtmlWebpackPlugin = require("html-webpack-plugin"),
  CleanWebpackPlugin = require("clean-webpack-plugin"),
  MiniCssExtractPlugin = require("mini-css-extract-plugin"),
  alias = require("./alias"),
  rules = require("./rules");

module.exports = {
  // 入口
  entry: "./src/index.js",
  // 输出
  output: {
    // 打包文件名
    filename: "[name].bundle.js",
    // 输出路径
    path: path.resolve(__dirname, "../dist"),
    // 资源请求路径
    publicPath: ""
  },
  module: {rules},
  plugins: [
    // 清除文件
    new CleanWebpackPlugin({
      dangerouslyAllowCleanPatternsOutsideProject: true,
      cleanOnceBeforeBuildPatterns: ["../dist/**"],
      dry: false
    }),
    // 提取样式文件
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename: "style/[name].[chunkhash:8].css",
      chunkFilename: "style/[id].css"
    }),
    new HtmlWebpackPlugin({
      // title
      title: "test",
      // 模板
      template: "index.html"
    })
  ],
  resolve: {
    // 创建 import 或 require 的别名,来确保模块引入变得更简单
    alias
  }
};

package.json

也稍微改一下执行路径, 换个更加合适的命令名

"scripts": {
    "dev": "webpack --config ./config/webpack.dev.js",
    "prod": "webpack --config ./config/webpack.prod.js",
    "server": "webpack-dev-server --config ./config/webpack.server.js"
},

为了实现配置效果我们需要安装一个插件 cross-env

yarn add cross-env

这是一个可以跨平台系统设置环境变量的库, 简单来说就是在命令行设置变量

"scripts": {
    "dev": "cross-env NODE_ENV=DEV webpack --config ./config/webpack.dev.js",
    "prod": "cross-env NODE_ENV=PROD webpack --config ./config/webpack.prod.js",
    "server": "cross-env NODE_ENV=SERVER webpack-dev-server --config ./config/webpack.server.js"
},

然后我们就能在 js 里获取 process.env.NODE_ENV 字段拿到我们自定义的字段了. 接下来我们修改一下配置文件

图片配置

我们目前的图片配置分别使用了 url-loader 转码和 image-webpack-loader 做压缩, 实际开发中我们不需要压缩, 所以将后者抽离.

{test: /\.(png|svg|jpe?g|gif)$/i, // 图片处理
  use:
    process.env.NODE_ENV === "PROD"
      ? [
          {
            loader: "url-loader",
            options: {name: "[name].[hash:5].[ext]",
              limit: 20 * 1024, // size <= 50kb
              outputPath: "img"
            }
          },
          {
            loader: "image-webpack-loader",
            options: {
              // Compress JPEG images
              mozjpeg: {
                progressive: true,
                quality: 65
              },
              // Compress PNG images
              optipng: {enabled: false},
              //  Compress PNG images
              pngquant: {
                quality: "65-90",
                speed: 4
              },
              // Compress GIF images
              gifsicle: {interlaced: false}
            }
          }
        ]
      : [
          {
            loader: "url-loader",
            options: {name: "[name].[hash:5].[ext]",
              limit: 20 * 1024, // size <= 50kb
              outputPath: "img"
            }
          }
        ]
},

REACT 热更新

引入 react 之后会发现现在修改 js 代码会刷新, 但是浏览器需要手动刷新才看到效果, 控制台提示

Ignored an update to unaccepted module,The following modules couldn’t be hot updated: (They would need a full reload!)

这个问题我们可以通过引入 react-hot-loader 解决

yarn add react-hot-loader

先在 .babelrc 添加配置

{
    "presets": [
        ["env", {modules: false}], "react"
    ],
    "plugins": ["react-hot-loader/babel"]
}

然后把根组件包裹在里面输出, 修改 \src\page\main.jsx 文件

import React, {Component, Fragment} from "react";
import {Switch, Route, Redirect, Link} from "react-router-dom";
import {hot} from "react-hot-loader";
import View1 from "CMT/view1";
import View2 from "CMT/view2";
import "STYLE/style.scss";
class Main extends Component {constructor(props, context) {super(props, context);
    this.state = {title: "Hello World!"};
  }

  render() {
    return (
      <Fragment>
        <p>{this.state.title}</p>
        <Link to="/view1/">View1</Link>
        <Link to="/view2/">View2</Link>
        <Switch>
          <Route exact path="/" component={View1} />
          <Route path="/view1/" component={View1} />
          <Route path="/view2/" component={View2} />
          <Redirect to="/" />
        </Switch>
      </Fragment>
    );
  }
}

export default hot(module)(Main);

然后重新执行命令测试即可

HTMl&CSS 热更新

如果足够认真的话你们会发现现在如果修改样式之后代码会更新, 但是浏览器不会自动刷新了.

那是因为热更新的代码是输出在内存中, 而我们之前引入了 mini-css-extract-plugin 插件提取 css 单独合并一个模块之后, 尽管代码也会更新, 但是 html 引用的 link 没有改变所以沿用的还是更新前的样式,html 更新暂时没找到方法, 但是不影响 React 开发, 而 css 更新我们可以用过环境配置, 不提取样式解决.

当然, 如果真的需要实现 html 更新的话, 可以简单粗暴的换回全局刷新即可

hot: true,
// hotOnly: true

rules.js

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

module.exports = [{test: /\.(js|jsx)$/, // 匹配文件
    exclude: /node_modules/, // 过滤文件夹
    use: {loader: "babel-loader"}
  }, {
    test: /\.s?css$/, // 匹配文件
    use: [process.env.NODE_ENV !== "SERVER" ? {
        loader: MiniCssExtractPlugin.loader,
        options: {
          // you can specify a publicPath here
          // by default it use publicPath in webpackOptions.output
          publicPath: '../'
        }
      } : 'style-loader', // 使用 <style> 将 css-loader 内部样式注入到我们的 HTML 页面,
      'css-loader', // 加载.css 文件将其转换为 JS 模块
      'sass-loader' // 加载 SASS / SCSS 文件并将其编译为 CSS
    ]
  },
  {test: /\.(html)$/,
    use: {
      loader: "html-loader",
      options: {attrs: ["img:src", "img:data-src", "audio:src"],
        minimize: true
      }
    }
  },
  {test: /\.(png|svg|jpe?g|gif)$/i, // 图片处理
    use:
      process.env.NODE_ENV === "PROD"
        ? [
            {
              loader: "url-loader",
              options: {name: "[name].[hash:5].[ext]",
                limit: 20 * 1024, // size <= 50kb
                outputPath: "img"
              }
            },
            {
              loader: "image-webpack-loader",
              options: {
                // Compress JPEG images
                mozjpeg: {
                  progressive: true,
                  quality: 65
                },
                // Compress PNG images
                optipng: {enabled: false},
                //  Compress PNG images
                pngquant: {
                  quality: "65-90",
                  speed: 4
                },
                // Compress GIF images
                gifsicle: {interlaced: false}
              }
            }
          ]
        : [
            {
              loader: "url-loader",
              options: {name: "[name].[hash:5].[ext]",
                limit: 20 * 1024, // size <= 50kb
                outputPath: "img"
              }
            }
          ]
  },
  {test: /\.(woff|woff2|eot|ttf|otf)$/, // 字体处理
    use: ["file-loader"]
  },
  {
    test: /\.xml$/, // 文件处理
    use: ["xml-loader"]
  }
]

webpack.common.js

const path = require("path"),
  HtmlWebpackPlugin = require("html-webpack-plugin"),
  CleanWebpackPlugin = require("clean-webpack-plugin"),
  MiniCssExtractPlugin = require("mini-css-extract-plugin"),
  alias = require("./alias"),
  rules = require("./rules");

module.exports = {
  // 入口
  entry: "./src/index.js",
  // 输出
  output: {
    // 打包文件名
    filename: "[name].bundle.js",
    // 输出路径
    path: path.resolve(__dirname, "../dist"),
    // 资源请求路径
    publicPath: ""
  },
  module: {rules},
  plugins: [
    // 清除文件
    new CleanWebpackPlugin({
      dangerouslyAllowCleanPatternsOutsideProject: true,
      cleanOnceBeforeBuildPatterns: ["../dist/**"],
      dry: false
    }),
    // 提取样式文件
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename:
        process.env.NODE_ENV !== "PROD"
          ? "[name].css"
          : "style/[name].[contenthash].css",
      chunkFilename:
        process.env.NODE_ENV !== "PROD"
          ? "[id].css"
          : "style/[id].[contenthash].css"
    }),
    new HtmlWebpackPlugin({
      // title
      title: "test",
      // 模板
      template: "index.html"
    })
  ],
  resolve: {
    // 创建 import 或 require 的别名,来确保模块引入变得更简单
    alias
  }
};

输出名字那里也换了一下, 官方推荐

For long term caching use filename: "[contenthash].css". Optionally add [name].

默认配置如下:

new MiniCssExtractPlugin({
    // Options similar to the same options in webpackOptions.output
    // both options are optional
    filename: devMode ? '[name].css' : '[name].[hash].css',
    chunkFilename: devMode ? '[id].css' : '[id].[hash].css',
})

但是这样子即使没改动也会导致 hash 改变而重新打包, 这里简单说一下几种常用变量的区别

  1. hash: 和整个项目构建相关并且全部文件公用相同 hash 值, 即没有缓存效果只适用于开发阶段
  2. chunkhash: 根据入口依赖文件解析构建对应的 chunk 生成对应的 hash 值, 可以保证正常业务修改不影响公共代码, 因为公共代码属于一个单独模块, 但是样式被打包进业务模块所以两者公用同一个 chunkhash.
  3. contenthash: 样式模块根据自身内容而生成, 做到不被业务代码改变而影响

因为对应打包路径换了一下, 所以 loader 也需要判断一些路径

{
  test: /\.s?css$/, // 匹配文件
  use: [
    process.env.NODE_ENV !== "SERVER"
      ? {
          loader: MiniCssExtractPlugin.loader,
          options: {
            // you can specify a publicPath here
            // by default it use publicPath in webpackOptions.output
            publicPath: process.env.NODE_ENV === "DEV" ? "./" : "../"
          }
        }
      : "style-loader", // 使用 <style> 将 css-loader 内部样式注入到我们的 HTML 页面,
    "css-loader", // 加载.css 文件将其转换为 JS 模块
    {
      loader: "postcss-loader",
      options: {
        config: {path: "./" // 写到目录即可,文件名强制要求是 postcss.config.js}
      }
    },
    "sass-loader" // 加载 SASS / SCSS 文件并将其编译为 CSS
  ]
},

生产警告!!!!

你们以为这样就算完了? 不, 你打包生产环境看看

npm run prod

然后你会发现居然没有打包 css!!??

到处查找资料发现有两种办法解决

修改引入方法

import "STYLE/style.scss"; -> require("STYLE/style.scss");

修改package.json

"sideEffects": ["*.scss", "*.css"]

具体原因可以看

CSS 压缩

mini-css-extract-plugin没有实现压缩功能, 我们自己重新引用一个完成库optimize-css-assets-webpack-plugin

yarn add optimize-css-assets-webpack-plugin

它会在构建期间搜索 css 资源并且优化压缩处理.

然后生产配置文件修改webpack.prod.js

const merge = require("webpack-merge"),
  common = require("./webpack.common.js"),
  OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = merge(common, {
  mode: 'production',
  // 原始源代码
  devtool: 'source-map',
  plugins: [new OptimizeCssAssetsPlugin()
  ]
});

CSS 增强

postcss-loader可以同通过配置增强 CSS 的功能, 在这里我们先简单使用自动补全前缀的功能, 首先

yarn add postcss-loader autoprefixer

在根目录新建 postcss.config.js 作为配置文件

const autoprefixer = require('autoprefixer');

module.exports = {
  plugins: [
    autoprefixer({browsers: ['iOS >= 6', 'Android >= 4', 'IE >= 9']
    })
  ]
};

只是加载插件设定兼容的系统版本, 同时也要在 rules.js 修改, 需要设定寻找配置的路径

{
    test: /\.s?css$/, // 匹配文件
    use: [process.env.NODE_ENV !== "SERVER" ? {
        loader: MiniCssExtractPlugin.loader,
        options: {
          // you can specify a publicPath here
          // by default it use publicPath in webpackOptions.output
          publicPath: '../'
        }
      } : 'style-loader', // 使用 <style> 将 css-loader 内部样式注入到我们的 HTML 页面,
      'css-loader', // 加载.css 文件将其转换为 JS 模块
      {
        loader: 'postcss-loader',
        options: {
          config: {path: './' // 写到目录即可,文件名强制要求是 postcss.config.js}
        }
      },
      'sass-loader' // 加载 SASS / SCSS 文件并将其编译为 CSS
    ]
 }

注意引入位置

Use it after css-loader and style-loader, but before other preprocessor loaders like e.g sass|less|stylus-loader, if you use any.

代理

启动服务器开发有时候需要访问外部域名请求, 但是后台又没帮你解决跨域问题的话, 我们可以再配置增加一个跨域配置, 如下

  devServer: {
    // 打开模式, Iframe mode 和 Inline mode 最后达到的效果都是一样的,都是监听文件的变化,然后再将编译后的文件推送到前端,完成页面的 reload 的
    inline: true,
    // 指定了服务器资源的根目录
    contentBase: path.join(__dirname, '../dist'),
    // 是否开启 gzip 压缩
    compress: false,
    port: 9000,
    // 是否开启热替换功能
    // hot: true,
    // 是否自动打开页面, 可以传入指定浏览器名字打开
    open: false,
    // 是否开启部分热替换功能
    hotOnly: true,
    proxy: {
      '/api': {
        // 代理地址
        target: 'http://alpha.xiaohuxi.cn',
        changeOrigin: true,
        // 默认情况下,不接受运行在 HTTPS 上,且使用了无效证书的后端服务器。如果你想要接受
        secure: true,
        // 重写路径
        pathRewrite: {'^/api': ''},
      }
    } 
  },

当下所有 /api 的请求都会被转发到 http://www.test.cn 地址去, 更多用法参考文档 http-proxy-middleware

退出移动版