美文网首页重学 webpack
第二章:webpack 进阶用法(1)

第二章:webpack 进阶用法(1)

作者: 晓风残月1994 | 来源:发表于2019-12-13 00:27 被阅读0次

演示仓库地址(可以翻 commit 记录):https://github.com/wangpeng1994/webpack-demo

  1. 自动清理构建目录产物
  2. PostCSS插件autoprefixer自动补齐CSS3前缀
  3. 移动端CSS px自动转换成rem
  4. 静态资源内联
  5. 多页面应用打包通用方案
  6. 使用sourcemap
  7. 提取页面公共资源

1. 自动清理构建目录产物

每次构建前不清理输出目录,会造成文件越来越多。

package.json 里的构建脚本中这样写:

rm -rf ./dist && webpack

更优雅的方式是使用 clean-webpack-plugin 插件,默认会删除 output 指定的输出目录:

// ...
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
//...
  plugins: [
    new CleanWebpackPlugin()
  ]
// ...

2. PostCSS插件autoprefixer自动补齐CSS3前缀

由于每个浏览器的标准化程度不同,使用新的 CSS 属性时为了兼容可能需要添加前缀。

image.png
.box {
  -moz-border-radius: 10px;
  -webkit-border-radius: 10px;
  -o-border-radius: 10px;
  border-radius: 10px;
}

loader 中增加 postcss-loader 并配置 autoprefixer(我的版本是"autoprefixer": "^9.7.3"),至少要在css-loader 将 css 转变为 commonjs 模块之前使用 postcss-loader,推荐放在紧挨着 css-loader 的位置(注意 use 数组中的 loader 是从尾向首的顺序执行):

// ...
  {
    test: /\.less$/,
    use: [
      MiniCssExtractPlugin.loader,
      'css-loader',
      {
        loader: 'postcss-loader',
        options: {
          plugins: [require('autoprefixer')]
        }
      },
      'less-loader',
    ]
  },
// ...

按照现版本的 autoprefixer 的建议,在 package.json 中增加 browserslist 字段:

  "browserslist": [
    "last 2 version",
    ">1%",
    "ios 7"
  ],

编译前:

.search-text{
  display:flex;
}

编译后可以看到已经成功加了前缀:

.search-text{display:-webkit-box;display:flex;}

3. 移动端CSS px自动转换成rem

使用 px2rem-loader 纯粹的做单位转换,使用 lib-flexible 库在页面加载时自动计算合理的根元素 font-size 大小(除了 rem 现在还有 vw 方案)。

// ...
  {
    test: /\.less$/,
    use: [
      MiniCssExtractPlugin.loader,
      'css-loader',
      {
        loader: 'postcss-loader',
        options: {
          plugins: [require('autoprefixer')]
        }
      },
      {
        loader: 'px2rem-loader',
        options: {
          remUnit: 75, // 1rem = 75px
          remPrecesion: 8 // 转为rem后,小数位个数
        }
      },
      'less-loader'
    ]
  },
// ...
.search-text{display:-webkit-box;display:flex;font-size:.266667rem;color:#00f}

4. 静态资源内联

资源内联的意义例举:

代码层面:保证重要的东西能随着 html 的加载一同完成,如页面框架的初始化脚本、数据埋点
工程维护:内联多页面时公共的 html 片段,小图片小字体内联
页面加载性能: 小图片小字体内联,减小 HTTP 网络请求数
页面加载体验: css 内联避免页面闪烁

css-loader 的作用是将 css 转换成 commonjs 对象,也就是样式代码会被放到 js 里面去了。style-loader 是代码运行时才动态的创建 style 标签并插入样式,而这里所说的静态资源内联是指在构建阶段完成的。为了方便维护,在开发时还是把文件独立出来编写,然后由 webpack 自动完成内联任务。

4.1 内联 HTML 和 JS

使用 raw-loader 读取文件字符串然后插入到 html 模板中(html-webpack-plugin 默认就是 ejs 模板引擎)。

斗鱼那里抄了些 meta.html:

<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<meta name="renderer" content="webkit">
<meta name="keywords"
  content="游戏直播,电竞直播,手游直播,lol直播,lpl直播,kpl直播,英雄联盟直播,dota2直播,dnf直播,炉石传说直播,cf直播,绝地求生直播,王者荣耀直播,游戏直播,赛事直播,美女直播,户外直播,二次元直播,视频直播,单机游戏直播">
<meta name="description"
  content="斗鱼 - 每个人的直播平台提供高清、快捷、流畅的视频直播和游戏赛事直播服务,包含英雄联盟lol直播、穿越火线cf直播、dota2直播、美女直播等各类热门游戏赛事直播和各种名家大神游戏直播,内容丰富,推送及时,带给你不一样的视听体验,一切尽在斗鱼 - 每个人的直播平台。">
<link rel="canonical" href="https://www.douyu.com">
<link rel="dns-prefetch" href="//shark.douyucdn.cn">
<link rel="dns-prefetch" href="//apic.douyucdn.cn">

准备将 meta.html 内联到下面的 index.html 模板挖好的完形填空中,该 index.html 模板会作为 html-webpack-plugin 的 template:

<!DOCTYPE html>
<html lang="en">
<head>
  <!-- 内联html -->
  ${require('raw-loader!./meta.html').default}
  <!-- 内联js -->
  <script>${require('raw-loader!../node_modules/lib-flexible/flexible.js').default}</script>
  <title>Document</title>
</head>
<body>
  
</body>
</html>

webpack.prod.js:

// ...
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: path.join(__dirname, 'src/index.html'),
      chunks: ['index'], // HTML 中只引入感兴趣的 chunk
      minify: {
        collapseWhitespace: true,
        preserveLineBreaks: false,
        minifyCSS: true, // 压缩源 html 文件中的内联 css
        minifyJS: true // 压缩源 html 文件中的内联 js
      }
    })
// ...

结果就是在 html-webpack-plugin 处理 html 模板的过程中,借助 raw-loader 把相关文件的字符串复制到了目标模板中,一起配合把坑填上了:


image.png

4.2 内联 CSS

CSS 内联的思路:

  1. 首先使用 mini-css-extract-plugin(而非 style-loader)将 css 提取打包成一个独立的 css chunk 文件
  2. 然后使用 html-webpack-plugin 生成 html 页面
  3. 最后使用 html-inline-css-webpack-plugin 读取打包好的 css chunk 内容注入到页面,原本 html-webpack-plugin 只会引入 css 资源地址,现在实现了 css 内联。

直接摘抄该插件的官方演示吧(用到的三个插件缺一不可):

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HTMLInlineCSSWebpackPlugin = require("html-inline-css-webpack-plugin").default;
 
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
      chunkFilename: "[id].css"
    }),
    new HtmlWebpackPlugin(),
    new HTMLInlineCSSWebpackPlugin(),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader"
        ]
      }
    ]
  }
}

4.3 内联图片、字体

第一章中介绍过了,最简单的做法就是使用 url-loader,让小于指定大小的图片或者字体文件在构建阶段自动 base64:

// ...
{
    test: /\.(png|jpg|gif|jpeg)$/,
    use: [
      {
        loader: 'url-loader',
        options: {
          limit: 10240, // 小于 10KB 会转为 base64 URIs,否则内部会自动调用 file-loader 
          name: '[name]_[hash:8].[ext]'
        }
      }
    ]
  },
  {
    test: /\.(woff|woff2|eot|ttf|otf)$/,
    use: [
      {
        loader: 'url-loader',
        options: {
          limit: 10240,
          name: '[name]_[hash:8].[ext]'
        }
      }
    ]
  }
// ...

再重复一遍,这里所说的静态资源内联是在构建阶段就完成了的。

静态资源内联参考目录:

  1. style-loader 注入 style 标签的相关源码
  2. webpack4 中如何实现资源内联@cpselvis/程柳锋

5. 多页面应用打包通用方案

每一次页面跳转,后端都会返回新的 html 文档,此为多页网站,亦为多页应用(MPA)。

优势:页面之间解耦,对 seo 友好。

思路:每个页面对应一个 entry,一个 html-webpack-plugin。

为了避免每次新增或删除页面需要修改 webpack 配置。因此需要动态获取 entry 和设置对应的 html-webpack-plugin。

做法很简单,使用 glob 库,通过 shell 风格的通配符去自动匹配文件路径。

主要是动态生成 entry 入口和 html-webpack-plugin 配置即可,这里约定每个页面都在 src 下面对应的文件夹中,每个文件夹的 html 模板都叫做 index.html,对应的入口 js 都叫做 index.js。

以下提供关键代码:

const glob = require('glob');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const setMPA = () => {
  const entry = {};
  const HtmlWebpackPlugins = [];

  const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js')); // 返回路径字符串数组

  entryFiles.forEach((pagePath, index) => {
    const pageName = pagePath.match(/\/([a-z\_\$]+)\/index.js$/)[1];
    entry[pageName] = pagePath;

    HtmlWebpackPlugins.push(
      new HtmlWebpackPlugin({
        filename: `${pageName}.html`,
        template: path.join(__dirname, `src/${pageName}/index.html`),
        chunks: [pageName],
        minify: {
          collapseWhitespace: true,
          preserveLineBreaks: false,
          minifyCSS: true,
          minifyJS: true
        }
      })
    );
  });

  return {
    entry,
    HtmlWebpackPlugins
  };
}

const { entry, HtmlWebpackPlugins } = setMPA();

module.exports = {
  // entry: {
  //  index: './src/index/index.js',
  //  search: './src/search/index.js'
  // }
  entry,
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name]_[chunkhash:8].js',
  },
  // ...
  plugins: [
    // ... other plugins
    ...HtmlWebpackPlugins,
    // ... other plugins
  ]
};
image.png

6. 使用sourcemap

作用:通过 sourcemap 定位到源代码用于排查 bug

开发环境开启,线上环境关闭,线上排查问题的时候可以将 sourcemap 上传到错误监控系统(如 Sentry)。

module.exports = {
  // ...
  devtool: 'source-map'
};

选项还挺多的,简单起见,开发时使用 source-map 这个即可,等遇到问题、瓶颈或知道哪里想要优化了,再回头研究其它选项也不迟。

命名规则:

  • eval:使用 eval 包裹模块代码
  • source map:产生 .map 文件
  • cheap:不包含列信息
  • inline:将 .map 作为 DataURl 嵌入,不单独生成 .map 文件
  • module:包含 loader 的 sourcemap
image.png

eval

打包出的 js 文件末尾,用 sourceURL 告知了对应的源文件路径:

// ...
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _helloworld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);\n\ndocument.write(Object(_helloworld__WEBPACK_IMPORTED_MODULE_0__[\"helloworld\"])());\nvar arr = [1, 2, 3];\nvar one = arr[0],\n    two = arr[1];\n\nvar foo = function foo() {\n  return console.log(one, two);\n};\n\nfoo();\nconsole.log(555);\n\n//# sourceURL=webpack:///./src/index/index.js?");

source-map

会有独立的 map 文件,在打包出的 js 文件最后一行可以看到对应的 map 文件路径:

// ...
//# sourceMappingURL=index_a5364e21.js.map

而在 map 文件中也能看到对应的 源 js:

{"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./src/index/index.js","webpack:///./src/index/helloworld.js"], /*后面略*/} 

inline-source-map

刚才的 sourcemap 内容内联到打包出的 js 文件中的最后一行,并且转为了 DataURL(base64):

// ...
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi  

7. 提取页面公共资源

这个配置项有些复杂,结合具体需求时再深究吧。继续前进,不能耽误整体学习进度。

optimization: {
    splitChunks: {
      minSize: 0,
      cacheGroups: {
        vendor: {
          test: /(react|react-dom)/, // 建议仅包括核心框架和实用程序
          name: 'vendors', // 打包文件名
          chunks: 'all' // 所有引入的库进行分离(推荐)
        },
        commons: {
          name: 'commons',
          chunks: 'all',
          minChunks: 2 // 只要引用两次就打包为一个文件
        }
      }
    }
image.png

相关文章

网友评论

    本文标题:第二章:webpack 进阶用法(1)

    本文链接:https://www.haomeiwen.com/subject/eeeagctx.html