1

前言

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

系列文章

webpack4从零开始构建(一)
webpack4+React16项目构建(二)
webpack4功能配置划分细化(三)
webpack4引入Ant Design和Typescript(四)
webpack4代码去重,简化信息和构建优化(五)
webpack4配置Vue版脚手架(六)

基本已经可以使用的完整配置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


Afterward
621 声望62 粉丝

努力去做,对的坚持,静待结果


引用和评论

0 条评论