乐趣区

关于webpack:Webpack-优化开发体验

Webpack Dev Server

集成了 主动编译 和 主动刷新浏览器 等性能。webpack-dev-server 在编译之后不会写入到任何输入文件,而是将 bundle 文件保留在内存中,而后将它们 serve 到 server 中。

yarn add webpack-dev-server --dev
const path = require("path");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");

module.exports = {
  mode: "development",
  entry: "./src/main.js",
  output: {
    filename: "bundle.js",
    path: path.join(__dirname, "dist"),
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /.png$/,
        use: {
          loader: "url-loader",
          options: {limit: 10 * 1024,},
        },
      },
    ],
  },
  plugins: [new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: "Ideal Webpack Develop Env",
      meta: {viewport: "width=device-width",},
      template: "./src/index.html",
    }),
    // 开发阶段最好不要应用这个插件,频繁复制动态资源文件
    // new CopyWebpackPlugin({//   patterns: ["public"],
    // }),
  ],
  devServer: {
    // 指定额定动态资源文件门路
    contentBase: ["./public"],
    proxy: {
      "/api": {
        // http://localhost:8080/api/users -> https://api.github.com/api/users
        target: "https://api.github.com",
        // http://localhost:8080/api/users -> https://api.github.com/users
        pathRewrite: {"^/api": "",},
      },
    },
  },
};
yarn webpack serve --open

Source Map

Source map 就是一个信息文件,外面贮存着地位信息。也就是说,转换后的代码的每一个地位,所对应的转换前的地位。

有了它,出错的时候,除错工具将间接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大不便。

启用 Source map 只须要在转换后的代码尾部加上 //# sourceMappingURL=/path/to/file.js.map

Webpack 中的 source map

const path = require("path");

module.exports = {
  mode: "development",
  entry: "./src/main.js",
  output: {
    filename: "bundle.js",
    path: path.join(__dirname, "dist"),
  },
  devtool: "source-map",
};

罕用模式:

  • eval:只能定位文件,没有生成 source map 文件,构建速度快,不能晓得行列信息
  • eval-source-map:能够定位文件和行列信息
  • eval-cheap-source-map:只能定位到行信息,代码通过 loader 转化
  • eval-cheap-module-source-map:开发环境的源代码

HMR(Hot Module Replacement)

HMR 容许在运行时更新所有类型的模块,而无需齐全刷新

const path = require("path");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");

module.exports = {
  mode: "development",
  entry: "./src/main.js",
  output: {
    filename: "bundle.js",
    path: path.join(__dirname, "dist"),
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /.png$/,
        use: {
          loader: "url-loader",
          options: {limit: 10 * 1024,},
        },
      },
    ],
  },
  plugins: [new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: "Ideal Webpack Develop Env",
      meta: {viewport: "width=device-width",},
      template: "./src/index.html",
    }),
  ],
  devServer: {
    hot: true,
    // hotOnly: true // 只应用 HMR,不会 fallback 到 live reloading
  },
  devtool: "source-map",
};

因为 css 具备对立的规定,只须要笼罩替换款式即可,而且 style-loader 曾经实现过 HMR 的热替换。而 js 文件是没有简单多样的,没有对立的规定,所以须要咱们本人依据理论状况实现

import "./main.css";
import createEditor from "./editor";

const editor = createEditor();
document.body.appendChild(editor);

// ================================================================
// HMR 手动解决模块热更新
// 不必放心这些代码在生产环境冗余的问题,因为通过 webpack 打包后,// 这些代码全副会被移除,这些只是开发阶段用到
if (module.hot) {
  let hotEditor = editor;
  module.hot.accept("./editor.js", () => {
    // 当 editor.js 更新,主动执行此函数
    // 长期记录编辑器内容
    const value = hotEditor.innerHTML;
    // 移除更新前的元素
    document.body.removeChild(hotEditor);
    // 创立新的编辑器
    // 此时 createEditor 曾经是更新过后的函数了
    hotEditor = createEditor();
    // 还原编辑器内容
    hotEditor.innerHTML = value;
    // 追加到页面
    document.body.appendChild(hotEditor);
  });

  module.hot.accept("./better.png", () => {
    // 当 better.png 更新后执行
    // 重写设置 src 会触发图片元素从新加载,从而部分更新图片
    img.src = background;
  });

  // style-loader 外部主动解决更新款式,所以不须要手动解决款式模块
}

不同环境下的配置

为不同环境创立不同的配置,应用 webpack-merge 合并雷同的配置

webpack.common.js

const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/main.js",
  output: {filename: "js/bundle.js",},
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
      {test: /\.(png|jpe?g|gif)$/,
        use: {
          loader: "file-loader",
          options: {
            outputPath: "img",
            name: "[name].[ext]",
          },
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: "Webpack Tutorial",
      template: "./src/index.html",
    }),
  ],
};

webpack.dev.js

const webpack = require("webpack");
const {merge} = require("webpack-merge");
const common = require("./webpack.common");

module.exports = merge(common, {
  mode: "development",
  devtool: "source-map",
  devServer: {
    hot: true,
    contentBase: "public",
  },
  plugins: [new webpack.HotModuleReplacementPlugin()],
});

webpack.prod.js

const {merge} = require("webpack-merge");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const common = require("./webpack.common");

module.exports = merge(common, {
  mode: "production",
  plugins: [new CleanWebpackPlugin(), new CopyWebpackPlugin(["public"])],
});

执行打包命令

yarn webpack --config webpack.dev.js

DefinePlugin

DefinePlugin 容许在 编译时 将你代码中的变量替换为其余值或表达式。这在须要依据开发模式与生产模式进行不同的操作时,十分有用。例如替换域名

    new webpack.DefinePlugin({
      // 值要求的是一个代码片段
      API_BASE_URL: JSON.stringify("https://api.example.com"),
    }),

留神,因为本插件会间接替换文本,因而提供的值必须在字符串自身中再蕴含一个理论的引号。通常,能够应用相似 '"production"' 这样的替换引号,或者间接用 JSON.stringify('production')

Tree Shaking

用于形容移除 JavaScript 上下文中的未援用代码。production 模式下曾经默认启用。

const path = require("path");
const webpack = require("webpack");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "none",
  entry: "./src/main.js",
  output: {
    filename: "bundle.js",
    path: path.join(__dirname, "dist"),
  },
  optimization: {
    // 模块只导出被应用的成员
    usedExports: true,
    // 压缩输入后果
    minimize: true,
  },
};

sideEffects

通过 package.json"sideEffects" 属性作为标记,向 compiler 提供提醒,表明我的项目中的哪些文件是 “pure(纯正 ES2015 模块)”,由此能够平安地删除文件中未应用的局部。

{"sideEffects": false}

如果你的代码的确有一些副作用,能够改为提供一个数组:

{"sideEffects": ["./src/some-side-effectful-file.js"]
}

“side effect(副作用)” 的定义是,在导入时会执行非凡行为的代码,而不是仅仅裸露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。

代码分包

多入口打包

实用于多页面打包,一个页面对应一个打包入口,提取公共局部

const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "none",
  entry: {
    index: "./src/index.js",
    album: "./src/album.js",
  },
  output: {filename: "[name].bundle.js",
  },
  optimization: {
    splitChunks: {
      // 主动提取所有公共模块到独自 bundle
      chunks: "all",
    },
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  plugins: [new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: "Multi Entry",
      template: "./src/index.html",
      filename: "index.html",
      chunks: ["index"],
    }),
    new HtmlWebpackPlugin({
      title: "Multi Entry",
      template: "./src/album.html",
      filename: "album.html",
      chunks: ["album"],
    }),
  ],
};

动静导入

动静导入的模块会被主动分包,实现按需加载

魔法正文:给分包的 bundle 定义名称

// import posts from './posts/posts'
// import album from './album/album'

const render = () => {
  const hash = window.location.hash || "#posts";

  const mainElement = document.querySelector(".main");

  mainElement.innerHTML = "";

  if (hash === "#posts") {// mainElement.appendChild(posts())
    import(/* webpackChunkName: 'components' */ "./posts/posts").then(({ default: posts}) => {mainElement.appendChild(posts());
      }
    );
  } else if (hash === "#album") {// mainElement.appendChild(album())
    import(/* webpackChunkName: 'components' */ "./album/album").then(({ default: album}) => {mainElement.appendChild(album());
      }
    );
  }
};

render();

window.addEventListener("hashchange", render);

CSS 文件压缩分包

yarn add terser-webpack-plugin optimize-css-assets-webpack-plugin mini-css-extract-plugin --dev
  • terser-webpack-plugin: 该插件应用 terser 来压缩 JavaScript
  • optimize-css-assets-webpack-plugin: 应用 cssnano 优化和压缩 CSS
  • mini-css-extract-plugin: 本插件会将 CSS 提取到独自的文件中,为每个蕴含 CSS 的 JS 文件创建一个 CSS 文件,并且反对 CSS 和 SourceMaps 的按需加载
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");

module.exports = {
  mode: "none",
  entry: {main: "./src/index.js",},
  output: {filename: "[name].bundle.js",
  },
  optimization: {
    minimizer: [
      // 申明数组会让 webpack 认为咱们须要自定义压缩,所以须要本人申明 js 文件的压缩
      new TerserWebpackPlugin(),
      new OptimizeCssAssetsWebpackPlugin(),],
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', // 将款式通过 style 标签注入
          MiniCssExtractPlugin.loader,
          "css-loader",
        ],
      },
    ],
  },
  plugins: [new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: "Dynamic import",
      template: "./src/index.html",
      filename: "index.html",
    }),
    new MiniCssExtractPlugin(),],
};

Hash 文件名

依赖文件名 hash 缓存。

我的项目级,我的项目任何改变都会变

  output: {filename: "[name]-[hash].js",
  },

chunk 级,同一 chunk 改变会援用同 chunk 的变动

  output: {filename: "[name]-[chunkhash].js",
  },

文件级,不同的文件不同的 hash 值,且 hash 长度为 8 位

  output: {filename: "[name]-[contenthash:8].js",
  },
退出移动版