webpack

154 阅读25分钟

1、什么是loader?常用loader以及配置

什么是loader?

  • Loader(加载器) 是 webpack 的核心之一。它用于将不同类型的文件转换为 webpack 可识别的模块。
  • loader 只是一个导出为函数的 JavaScript 模块,loader runner 会调用这个函数,然后把上一个 loader 产生的结果或者资源文件(resource file)传入进去。
// loaders/simple-loader.js
module.exports = function loader (source) {
    console.log('simple-loader is working');
    return source;
}

CSS相关loader

  • MiniCssExtractPlugin.loader: 分离 css 样式。把 js 中 import 导入的样式文件代码,打包成一个实际的 css 文件,结合 html-webpack-plugin,在 dist/index.html 中以 link 插入 css 文件;默认将 js 中 import 的多个 css 文件,打包时合成一个。
  • style-loader: 把 js 中 import 导入的样式文件代码,打包到 js 文件中,运行 js 文件时,将样式自动插入到style标签中。
  • css-loader: 用来处理 CSS 文件,会对 @import 和 url() 进行处理,就像 js 解析 import/require() 一样。
  • postcss-loader: 结合autoprefixer插件,可以让 css 自动加上兼容性前缀。
  • px2rem-loader: 用于移动端适配,自动将 px 转换成 rem。
  • sass-loader: 加载 Sass/SCSS 文件并将他们编译为 CSS。
/**
 * css 公共规则
 */
function getBaseCssRules(importLoaders = 1, modules = false) {
  return [
    isDev ? "style-loader" : MiniCssExtractPlugin.loader, // style-loader和MiniCssExtractPlugin.loader冲突
    {
      loader: "css-loader",
      options: {
        modules: modules, // 是否模板化css,模块化css会被重新命名。例如index.module.css被模块化
        importLoaders, // 在css-loader前应用的loader的数目, 默认为0
        sourceMap: isDev,
      },
    },
    {
      loader: "postcss-loader",
      options: {
        postcssOptions: {
          plugins: [
            require("autoprefixer")({
              overrideBrowserslist: [">0.25%", "not dead"],
            }),
          ],
          sourceMap: isDev,
        },
      },
    },
  ];
}

const scssRule = {
  test: /\.(sc|sa)ss$/,
  use: [...getBaseCssRules(2), "sass-loader"],
  exclude: /\.module\.(sc|sa)ss$/,
};

js、ts、jsx、tsx相关loader

  • babel-loader: 用来处理ES6语法,将其编译为浏览器可以执行的js语法。
  • @babel/core: 是Babel进行转码的核心依赖包。
  • @babel/preset-env: 是一个智能预设,可让您使用最新的JavaScript,而无需微观管理目标环境所需的语法转换(以及可选的浏览器polyfill)。

useBuiltIns:

  • false: 默认值,不引入 polyfill。

  • entry: 一种入口导入方式, 只要我们在打包配置入口 或者 文件入口写入 import "core-js" 这样一串代码, babel 就会替我们根据当前你所配置的目标浏览器(browserslist 配置)来引入所需要的 polyfill。

  • usage: 即“按需引用”,babel 可以按需加载 polyfill,并且不需要手动引入 @babel/polyfill。

  • @babel-polyfill: 通过向全局对象和内置对象的prototype上添加方法来实现的。所以这会造成全局空间污染。
  • @babel/plugin-transform-runtime: 作用是转译代码,转译后的代码中可能会引入 @babel/runtime-corejs3 里面的模块。前者运行在编译时,后者运行在运行时。局部使用es6的函数或方法。
  • @babel/runtime-corejs3: polyfill 是局部文件中以引用的形式存在不会污染全局变量。
  • @babel/preset-typescript: 使用ts时使用。
  • @babel/preset-react: react项目中使用。
/**
 * js 和 jsx的loader
 */
const jsxRule = {
  test: /\.jsx?$/,
  use: ["babel-loader"],
  exclude: /node_modules/, //排除 node_modules 目录
};

const tsxRule = {
  test: /\.tsx?$/,
  use: ["babel-loader"],
  exclude: /node_modules/, //排除 node_modules 目录
};
// .babelrc babel配置文件
{
  // * 执行顺序从后往前。
  "presets": [
    [
      // * 使用 polyfill 代替一些浏览器不能识别的 ES 新的 API。
      "@babel/preset-env",
      {
        // ! 防止 babel 将任何模块类型都转译成 CommonJS 类型,导致 tree-shaking 失效问题。
        "modules": false
      }
    ],
    "@babel/preset-react",
    "@babel/preset-typescript"
  ],
  "plugins": [
    [
      // 主要防止定义很多重复代码,polyfill
      // 其中 @babel/plugin-transform-runtime 的作用是转译代码,转译后的代码中可能会引入 @babel/runtime-corejs3 里面的模块。所以前者运行在编译时,后者运行在运行时。类似 polyfill,后者需要被打包到最终产物里在浏览器中运行。
      "@babel/plugin-transform-runtime",
      {
        "corejs": {
          "version": 3,
          "proposals": true
        },
        "useESModules": true
      }
    ],
    // 有时候我们将 defaultProps, propTypes写在class中,而不是分开写
    // 主要解决react中class组件内部使用箭头函数绑定this的问题
    "transform-class-properties"
  ]
}

文件相关loader

  • file-loader: 处理导入的文件,修改打包后图片的储存路径,再根据配置修改我们引用的路径,使之对应引入。
  • url-loader: url-loader封装了file-loader。并添加limit属性配置,把文件以base64的形式打包到js文件中。
/**
 * 图片文字处理
 */
const imageRule = {
  test: /\.(png|jpg|gif|jpeg|webp|svg)$/,
  use: [
    {
      loader: "url-loader",
      options: {
        limit: 1024, //10K
        name: "[name].[contenthash:8].[ext]",
        outputPath: "assets/images",
        // esModule 设置为 false,否则,<img src={require('XXX.jpg')} /> 会出现 <img src=[Module Object] />
        esModule: false,
      },
    },
  ],
};

const textRule = {
  test: /\.(ttf|woff|woff2|eot|otf)$/,
  use: [
    {
      loader: "url-loader",
      options: {
        name: "[name].[contenthash:8].[ext]", //打包后的文件名称
        outputPath: "assets/fonts", // 打包后输出目录
      },
    },
  ],
};

HTML相关loader

  • html-withimg-loader: 解决html中的img的src引入的图片无法正常打包。在file-loader或url-loader 的options中必须配置esModule: false。
// 解决html文件中引入图片的问题
const htmlRule = {
  test: /.html$/,
  use: "html-withimg-loader",
};

自定义loader

webpack配置案例

// rules.config.js 所有用到的loader
const { isDev } = require("../constants");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

/**
 * js 和 jsx的loader
 */
const jsxRule = {
  test: /\.jsx?$/,
  use: ["babel-loader"],
  exclude: /node_modules/, //排除 node_modules 目录
};

const tsxRule = {
  test: /\.tsx?$/,
  use: ["babel-loader"],
  exclude: /node_modules/, //排除 node_modules 目录
};
/**
 * css 公共规则
 */
function getBaseCssRules(importLoaders = 1, modules = false) {
  return [
    isDev ? "style-loader" : MiniCssExtractPlugin.loader, // style-loader和MiniCssExtractPlugin.loader冲突
    {
      loader: "css-loader",
      options: {
        modules: modules, // 是否模板化css,模块化css会被重新命名。例如index.module.css被模块化
        importLoaders, // 在css-loader前应用的loader的数目, 默认为0
        sourceMap: isDev,
      },
    },
    {
      loader: "postcss-loader",
      options: {
        postcssOptions: {
          plugins: [
            require("autoprefixer")({
              overrideBrowserslist: [">0.25%", "not dead"],
            }),
          ],
          sourceMap: isDev,
        },
      },
    },
  ];
}

const cssRule = {
  test: /\.css$/,
  use: getBaseCssRules(),
  exclude: /node_modules/,
};

// * 单独处理 antd 样式,避免模块化 css 文件影响。
const nodeCssRule = {
  test: /\.css$/,
  use: [
    'style-loader',
    {
      loader: 'css-loader',
      options: {
        sourceMap: isDev
      }
    }
  ],
  exclude: /src/
}

const scssRule = {
  test: /\.(sc|sa)ss$/,
  use: [...getBaseCssRules(2), "sass-loader"],
  exclude: /\.module\.(sc|sa)ss$/,
};

const lessRule = {
  test: /\.less$/,
  use: [...getBaseCssRules(2), "less-loader"],
  exclude: /\.module\.less$/,
};

// modules
const scssModuleRule = {
  test: /\.module\.(sc|sa)ss$/,
  use: [...getBaseCssRules(2, true), "sass-loader"],
};

const lessModuleRule = {
  test: /\.module\.less$/,
  use: [...getBaseCssRules(2, true), "less-loader"],
};

/**
 * 图片文字处理
 */

const imageRule = {
  test: /\.(png|jpg|gif|jpeg|webp|svg)$/,
  use: [
    {
      loader: "url-loader",
      options: {
        limit: 1024, //10K
        name: "[name].[contenthash:8].[ext]",
        outputPath: "assets/images",
        // esModule 设置为 false,否则,<img src={require('XXX.jpg')} /> 会出现 <img src=[Module Object] />
        esModule: false,
      },
    },
  ],
};

const textRule = {
  test: /\.(ttf|woff|woff2|eot|otf)$/,
  use: [
    {
      loader: "url-loader",
      options: {
        name: "[name].[contenthash:8].[ext]",
        outputPath: "assets/fonts",
      },
    },
  ],
};

// 解决html文件中引入图片的问题
const htmlRule = {
  test: /.html$/,
  use: "html-withimg-loader",
};

module.exports = {
  jsxRule,
  cssRule,
  nodeCssRule,
  scssRule,
  scssModuleRule,
  lessRule,
  lessModuleRule,
  imageRule,
  textRule,
  htmlRule,
  tsxRule,
};

// webpack.common.js 基础配置
const webpack = require("webpack");
const path = require("path");
const { ROOTPATH, isDev, isBuildParse } = require("../constants");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { htmlConfig } = require("../config");
const Rules = require("./rules.config");
const WebpackBar = require("webpackbar");

let outputDir = path.resolve(ROOTPATH, "build");
let publicPath = isDev ? '/' : process.env.PUBLIC_URL;
let entry = {
  index: path.resolve(ROOTPATH, "src/pages/edit/index.tsx"),
  parse: path.resolve(ROOTPATH, "src/pages/parse/index.tsx"),
};
let indexHtmlPlugin = new HtmlWebpackPlugin({
  template: path.resolve(ROOTPATH, "public/index.html"),
  filename: "index.html",
  chunks: ['index'],
  minify: {
    removeAttributeQuotes: false, //是否删除属性的双引号
    collapseWhitespace: false, //是否折叠空白
  },
  config: htmlConfig[isDev ? "dev" : "build"],
});

let parseHtmlPlugin = new HtmlWebpackPlugin({
  template: path.resolve(ROOTPATH, "public/index.html"),
  filename: "parse.html",
  chunks: ['parse'],
  minify: {
    removeAttributeQuotes: false, //是否删除属性的双引号
    collapseWhitespace: false, //是否折叠空白
  },
  config: htmlConfig[isDev ? "dev" : "build"],
});

let parseHtmlPluginSingle = new HtmlWebpackPlugin({
  template: path.resolve(ROOTPATH, "public/parse.html"),
  filename: "parse.html",
  chunks: ['parse'],
  minify: {
    removeAttributeQuotes: false, //是否删除属性的双引号
    collapseWhitespace: false, //是否折叠空白
  },
  config: htmlConfig[isDev ? "dev" : "build"],
});
let htmlWebpackPlugins = [indexHtmlPlugin, parseHtmlPlugin];

// 单独打包解析
if (isBuildParse) {
  publicPath = './';
  outputDir = path.resolve(ROOTPATH, "lib/parse");
  entry = {
    parse: path.resolve(ROOTPATH, "src/pages/parse/index.tsx"),
  };
  htmlWebpackPlugins = [parseHtmlPluginSingle];
}

module.exports = {
  entry,
  output: {
    filename: "js/[name]/[name]-bundle.js",
    path: outputDir,
    publicPath: publicPath,
  },
  resolve: {
    // * 配置后引入模块时,不需要加入后缀。
    extensions: [".tsx", ".ts", ".js", ".json"],
    // * 文件别名配置,需同步 tsconfig.json 中的映射路径配置。
    alias: {
      '@': path.resolve(ROOTPATH, "./src"),
    },
  },
  module: {
    rules: [
      Rules.tsxRule,
      Rules.jsxRule,
      Rules.cssRule,
      Rules.nodeCssRule,
      Rules.scssRule,
      Rules.scssModuleRule,
      Rules.lessRule,
      Rules.lessModuleRule,
      Rules.imageRule,
      Rules.textRule,
      // Rules.htmlRule, // html-withimg-loader处理后无法在html中使用ejs等语法
    ],
  },
  plugins: [
    ...htmlWebpackPlugins,
    new CopyWebpackPlugin({
      patterns: [
        {
          context: path.resolve(ROOTPATH, "./public"),
          from: "*",
          to: path.resolve(ROOTPATH, "./build"),
          toType: "dir",
          globOptions: {
            ignore: ["**/index.html", "**/parse.html"],
          },
        },
      ],
    }),
    // 应用中需要的process.env变量,在此注入才能使用。
    new webpack.DefinePlugin({
      'process.env.RUNTIME_ENV': JSON.stringify(process.env.RUNTIME_ENV),
    }),
    // * 控制台显示编译/打包进度。
    new WebpackBar({
      name: "build",
      color: "#fa8c16",
    }),
    !isDev &&
      // * css 样式拆分,抽离公共代码。
      new MiniCssExtractPlugin({
        filename: "css/[name].[contenthash:8].css",
        chunkFilename: "css/[name].[contenthash:8].css",
        ignoreOrder: false,
      }),
  ].filter(Boolean),
};

// webpack.dev.js
const webpack = require('webpack');
const { devPort } = require("../constants");
const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");

module.exports = merge(common, {
  mode: "development",
  devtool: "eval-cheap-module-source-map",
  devServer: {
    historyApiFallback: true,
    port: devPort, //默认是8080
    hot: true, // 热更新第一步
    quiet: false, //默认不启用
    inline: true, //默认开启 inline 模式,如果设置为false,开启 iframe 模式
    stats: "errors-only", //终端仅打印 error
    overlay: false, //默认不启用
    clientLogLevel: "silent", //日志等级
    compress: true, //是否启用 gzip 压缩
    proxy: {
      "/api": {
        target: "http://print.bb.test.sankuai.com",
        changeOrigin: true,
      },
    },
  },
  plugins: [
    // * 热更新第二步:引入插件,此时会全量更新;需要局部更新要在入口文件进一步设置。
    new webpack.HotModuleReplacementPlugin()
  ]
});
webpack.prod.js 线上配置
const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");

module.exports = merge(common, {
  mode: "production"
});

1、什么是webpack插件?常用plugin以及配置的

什么是插件?

  • 何为插件(Plugin)?专注处理 webpack 在编译过程中的某个特定的任务的功能模块,可以称为插件。
  • 插件是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在 整个 编译生命周期都可以访问 compiler 对象。
  • Plugin 是一个扩展器,它丰富了 webpack 本身,针对是 loader 结束后,webpack 打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些节点,执行广泛的任务。

Plugin 的特点:

  • 是一个独立的模块,模块对外暴露一个 js 函数。
  • 函数的原型 (prototype) 上定义了一个注入 compiler 对象的 apply方法 apply 函数中需要有通过 compiler 对象挂载的 webpack 事件钩子,钩子的回调中能拿到当前编译的 compilation 对象,如果是异步编译插件的话可以拿到回调 callback。
  • 完成自定义子编译流程并处理 complition 对象的内部数据。
  • 如果异步编译插件的话,数据处理完成后执行 callback 回调。
// ConsoleLogOnBuildWebpackPlugin.js
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    compiler.hooks.run.tap(pluginName, (compilation) => {
      console.log('webpack 构建正在启动!');
    });
  }
}

module.exports = ConsoleLogOnBuildWebpackPlugin;

HotModuleReplacementPlugin 模块热更新插件

Hot-Module-Replacement 的热更新是依赖于 webpack-dev-server,后者是在打包文件改变时更新打包文件或者 reload 刷新整个页面,HRM 是只更新修改的部分。

HotModuleReplacementPlugin是webpack模块自带的,所以引入webpack后,在plugins配置项中直接使用即可。

module.exports = merge(common, {
  mode: "development",
  devtool: "eval-cheap-module-source-map",
  devServer: {
    historyApiFallback: true,
    port: devPort, //默认是8080
    hot: true, // 热更新第一步
    quiet: false, //默认不启用
    inline: true, //默认开启 inline 模式,如果设置为false,开启 iframe 模式
    stats: "errors-only", //终端仅打印 error
    overlay: false, //默认不启用
    clientLogLevel: "silent", //日志等级
    compress: true, //是否启用 gzip 压缩
    proxy: {
      "/api": {
        target: "http://print.bb.test.sankuai.com",
        changeOrigin: true,
      },
    },
  },
  plugins: [
    // * 热更新第二步:引入插件,此时会全量更新;需要局部更新要在入口文件进一步设置。
    new webpack.HotModuleReplacementPlugin()
  ]
});

html-webpack-plugin 生成 html 文件

将 webpack 中entry配置的相关入口 chunk 和 extract-text-webpack-plugin抽取的 css 样式 插入到该插件提供的template或者templateContent配置项指定的内容基础上生成一个 html 文件,具体插入方式是将样式link插入到head元素中,script插入到head或者body中。

inject 有四个选项值:

  • true:默认值,script 标签位于 html 文件的 body 底部
  • body:script 标签位于 html 文件的 body 底部(同 true)
  • head:script 标签位于 head 标签内
  • false:不插入生成的 js 文件,只是单纯的生成一个 html 文件
const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins: [
  new HtmlWebpackPlugin({
    filename: 'index.html', // 生成的html文件名称
    template: path.join(__dirname, '/index.html'), // 基础模板
    minify: { // 压缩HTML文件
      removeComments: true, // 移除HTML中的注释
      collapseWhitespace: true, // 删除空白符与换行符
      minifyCSS: true, // 压缩内联css
    },
    inject: true, // js文件注入位置,
  }),
]

多页应用打包

let outputDir = path.resolve(ROOTPATH, "build");
let publicPath = isDev ? '/' : process.env.PUBLIC_URL;

// 两个入口
let entry = {
  index: path.resolve(ROOTPATH, "src/pages/edit/index.tsx"),
  parse: path.resolve(ROOTPATH, "src/pages/parse/index.tsx"),
};

// 两个html插件配置
let indexHtmlPlugin = new HtmlWebpackPlugin({
  template: path.resolve(ROOTPATH, "public/index.html"),
  filename: "index.html",
  chunks: ['index'], // 插入html的js入口文件
  minify: {
    removeAttributeQuotes: false, //是否删除属性的双引号
    collapseWhitespace: false, //是否折叠空白
  },
  config: htmlConfig[isDev ? "dev" : "build"],
});

let parseHtmlPlugin = new HtmlWebpackPlugin({
  template: path.resolve(ROOTPATH, "public/index.html"),
  filename: "parse.html",
  chunks: ['parse'], // 插入html的js入口文件
  minify: {
    removeAttributeQuotes: false, //是否删除属性的双引号
    collapseWhitespace: false, //是否折叠空白
  },
  config: htmlConfig[isDev ? "dev" : "build"],
});

clean-webpack-plugin

用于在打包前清理上一次项目生成的 bundle 文件,它会根据 output.path 自动清理文件夹;这个插件在生产环境用的频率非常高,因为生产环境经常会通过 hash 生成很多 bundle 文件,如果不进行清理的话每次都会生成新的,导致文件夹非常庞大。

const { CleanWebpackPlugin } = require('clean-webpack-plugin')

plugins: [
  new HtmlWebpackPlugin({
    template: path.join(__dirname, '/index.html'),
  }),
  new CleanWebpackPlugin(), // 所要清理的文件夹名称
]

mini-css-extract-plugin 将 CSS 提取为独立的文件

将 CSS 提取为独立的文件的插件,对每个包含 css 的 js 文件都会创建一个 CSS 文件,支持按需加载 css 和 sourceMap。只能用在 webpack4 中。

这个插件应该只用在生产环境配置,并且在 loaders 链中不使用 style-loader, 而且这个插件暂时不支持 HMR。

function getBaseCssRules(importLoaders = 1, modules = false) {
  return [
    isDev ? "style-loader" : MiniCssExtractPlugin.loader, // style-loader和MiniCssExtractPlugin.loader冲突
    {
      loader: "css-loader",
      options: {
        modules: modules, // 是否模板化css,模块化css会被重新命名。例如index.module.css被模块化
        importLoaders, // 在css-loader前应用的loader的数目, 默认为0
        sourceMap: isDev,
      },
    },
    {
      loader: "postcss-loader",
      options: {
        postcssOptions: {
          plugins: [
            require("autoprefixer")({
              overrideBrowserslist: [">0.25%", "not dead"],
            }),
          ],
          sourceMap: isDev,
        },
      },
    },
  ];
}

	// loaders
	module: {
		rules: [
			{
				test: /\.css$/,
			   use: getBaseCssRules(),
			   exclude: /node_modules/,
			}
		]
	}
	plugins: [
    // ...
    !isDev &&
      // * css 样式拆分,抽离公共代码。
      new MiniCssExtractPlugin({
        filename: "css/[name].[contenthash:8].css",
        chunkFilename: "css/[name].[contenthash:8].css",
        ignoreOrder: false,
      }),
  ]

webpack.DefinePlugin 定义全局的变量

DefinePlugin 是 webpack 自带的插件。可以定义一些全局的变量,我们可以在模块当中直接使用这些变量。

plugins: [
  new webpack.DefinePlugin({
    DESCRIPTION: 'This Is The Test Text.',
  }),
]

// 直接引用
console.log(DESCRIPTION)

copy-webpack-plugin 拷贝文件

const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
  plugins: [
    new CopyWebpackPlugin({
      patterns: [
      	  // 将public目录copy到build目录,移出部分文件
        {
          context: path.resolve(ROOTPATH, "./public"),
          from: "*",
          to: path.resolve(ROOTPATH, "./build"),
          toType: "dir",
          globOptions: {
            ignore: ["**/index.html", "**/parse.html"],
          },
        },
      ],
    }),
  ],
}

webpackbar 时实时显示打包进度

// * 控制台显示编译/打包进度。
new WebpackBar({
  name: "build",
  color: "#fa8c16",
}),

自定义插件实现

1、webpack的TreeShaking

什么是 Tree Shaking

Tree-Shaking 是一种基于 ES Module 规范的 Dead Code Elimination 技术,它会在运行过程中静态分析模块之间的导入导出,确定 ESM 模块中哪些导出值未曾其它模块使用,并将其删除,以此实现打包产物的优化。

在 Webpack 中启动 Tree Shaking

  • 使用 ESM 规范编写模块代码
  • 配置 optimization.usedExports 为 true,启动标记功能

启动代码优化功能,可以通过如下方式实现:

  • 配置 mode = production
  • 配置 optimization.minimize = true
  • 提供 optimization.minimizer 数组

为什么只能对ESModule做TreeShaking

ES6 module 特点:

  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量
  • import binding 是 immutable的

ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。

所谓静态分析就是不执行代码,从字面量上对代码进行分析,ES6之前的模块化,比如我们可以动态require一个模块,只有执行后才知道引用的什么模块,这个就不能通过静态分析去做优化。这是 ES6 modules 在设计时的一个重要考量,也是为什么没有直接采用 CommonJS,正是基于这个基础上,才使得 tree-shaking 成为可能,这也是为什么 rollup 和 webpack 2 都要用 ES6 module syntax 才能 tree-shaking。

1、webpack的SplitChunk

什么是chunks

一个常考的面试题是 module、chunk、bundle是什么?

  • 一个ts文件、图片、less、pug等都是一个module,而打包后的产物,总的称呼就是bundle。
  • 对于打包产物bundle, 有些情况下,我们觉得太大了。 为了优化性能,比如快速打开首屏,利用缓存等,我们需要对bundle进行以下拆分,对于拆分出来的东西,我们叫它chunk。

webpack与grunt、gulp的不同

Grunt、Gulp是基于任务运⾏的⼯具: 它们会⾃动执⾏指定的任务,就像流⽔线,把资源放上去然后通过不同插件进⾏加⼯,它们包含活跃的社区,丰富的插件,能⽅便的打造各种⼯作流。

***Webpack是基于模块化打包的⼯具: **⾃动化处理模块, webpack把⼀切当成模块,当 webpack 处理应⽤程序时,它会递归地构建⼀个依赖关系图 (dependency graph),其中包含应⽤程序需要的每个模块,然后将所有这些模块打包成⼀个或多个 bundle。 因此这是完全不同的两类⼯具,⽽现在主流的⽅式是⽤npm script代替Grunt、Gulp,npm script同样可以打造任务流.

Loader和Plugin的不同?

不同的作⽤:

  • Loader直译为"加载器"。Webpack将⼀切⽂件视为模块,但是webpack原⽣是只能解析js⽂件,如果想将其他⽂件 也打包的话,就会⽤到 loader 。 所以Loader的作⽤是让webpack拥有了加载和解析⾮JavaScript⽂件的能⼒。

  • Plugin直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运⾏的⽣命周期中会⼴播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

不同的⽤法:

  • Loader在 module.rules 中配置,也就是说他作为模块的解析规则⽽存在。 类型为数组,每⼀项都是⼀ 个 Object ,⾥⾯描述了对于什么类型的⽂件( test ),使⽤什么加载( loader )和使⽤的参数( options )

  • Plugin在 plugins 中单独配置。 类型为数组,每⼀项是⼀个 plugin 的实例,参数都通过构造函数传⼊。

webpack的构建流程是什么

Webpack 的运⾏流程是⼀个串⾏的过程,从启动到结束会依次执⾏以下流程:

  • 1、 初始化参数:从配置⽂件和 Shell 语句中读取与合并参数,得出最终的参数;

  • 2、 开始编译:⽤上⼀步得到的参数初始化 Compiler 对象,加载所有配置的插件,执⾏对象的 run ⽅法开始执⾏编译;

  • 3、 确定⼊⼝:根据配置中的 entry 找出所有的⼊⼝⽂件;

  • 4、 编译模块:从⼊⼝⽂件出发,调⽤所有配置的 Loader 对模块进⾏翻译,再找出该模块依赖的模块,再递归本步骤 直到所有⼊⼝依赖的⽂件都经过了本步骤的处理;

  • 5、 完成模块编译:在经过第4步使⽤ Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;

  • 6、 输出资源:根据⼊⼝和模块之间的依赖关系,组装成⼀个个包含多个模块的 Chunk,再把每个 Chunk 转换成⼀个单独的⽂件加⼊到输出列表,这步是可以修改输出内容的最后机会;

  • 7、 输出完成:在确定好输出内容后,根据配置确定输出的路径和⽂件名,把⽂件内容写⼊到⽂件系统。

在以上过程中,Webpack 会在特定的时间点⼴播出特定的事件,插件在监听到感兴趣的事件后会执⾏特定的逻辑,并且插件可以调⽤ Webpack 提供的 API 改变 Webpack 的运⾏结果。

webpack的热更新是如何做到的?说明其原理?

webpack的热更新⼜称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不⽤刷新浏览器⽽将新变更的模块替换掉旧的模块。

原理图: image.png

⾸先要知道server端和client端都做了处理⼯作

  • 1、 第⼀步,在 webpack 的 watch 模式下,⽂件系统中某⼀个⽂件发⽣修改,webpack 监听到⽂件变化,根据配置⽂ 件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。

  • 2、 第⼆步是 webpack-dev-server 和 webpack 之间的接⼝交互,⽽在这⼀步,主要是 dev-server 的中间件 webpack- dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调⽤ webpack 暴露的 API对代码变化进⾏监 控,并且告诉 webpack,将代码打包到内存中。

  • 3、 第三步是 webpack-dev-server 对⽂件变化的⼀个监控,这⼀步不同于第⼀步,并不是监控代码变化重新打包。当 我们在配置⽂件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置⽂件夹中静态⽂件 的变化,变化后会通知浏览器端对应⽤进⾏ live reload。注意,这⼉是浏览器刷新,和 HMR 是两个概念。

  • 4、 第四步也是 webpack-dev-server 代码的⼯作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器 端和服务端之间建⽴⼀个 websocket ⻓连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也 包括第三步中 Server 监听静态⽂件变化的信息。浏览器端根据这些 socket 消息进⾏不同的操作。当然服务端传递 的最主要信息还是新模块的 hash 值,后⾯的步骤根据这⼀ hash 值来进⾏模块热替换。

  • 5、 webpack-dev-server/client 端并不能够请求更新的代码,也不会执⾏热更模块操作,⽽把这些⼯作⼜交回给了 webpack,webpack/hot/dev-server 的⼯作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进⾏模块热更新。当然如果仅仅是刷新浏览器,也就没有后⾯那些步骤了。

  • 6、 HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上⼀步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回⼀个 json,该 json 包含了所有要更新的模块 的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步 骤。

  • 7、 ⽽第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进⾏对⽐,决定是 否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引⽤。

  • 8、 最后⼀步,当 HMR 失败后,回退到 live reload 操作,也就是进⾏浏览器刷新来获取最新打包代码。

如何⽤webpack来优化前端性能?

⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。

  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css

  • 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对 于 output 参数和各loader的 publicPath 参数来修改资源路径

  • Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来 实现

  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存

  • 提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的 公共代码

如何提⾼webpack的打包速度?

  • happypack: 利⽤进程并⾏编译loader,利⽤缓存来使得 rebuild 更快,遗憾的是作者表示已经不会继续开发此项⽬,类 似的替代者是thread-loader

  • 压缩速度:使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度

  • 外部扩展(externals): 将不怎么需要更新的第三⽅库脱离webpack打包,不被打⼊bundle中,从⽽减少打包时间,⽐ 如jQuery⽤script标签引⼊

  • dll: 采⽤webpack的 DllPlugin 和 DllReferencePlugin 引⼊dll,让⼀些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间

  • 利⽤缓存: webpack.cache 、babel-loader.cacheDirectory、 HappyPack.cache 都可以利⽤缓存提⾼rebuild效率

  • 缩⼩⽂件搜索范围: ⽐如babel-loader插件,如果你的⽂件仅存在于src中,那么可以 include: path.resolve(__dirname, 'src') ,当然绝⼤多数情况下这种操作的提升有限,除⾮不⼩⼼build了node_modules⽂件

Babel的原理是什么?

babel 的转译过程也分为三个阶段,这三步具体是:

  • 解析 Parse: 将代码解析⽣成抽象语法树( 即AST ),即词法分析与语法分析的过程

  • 转换 Transform: 对于 AST 进⾏变换⼀系列的操作,babel 接受得到 AST 并通过 babel-traverse 对其进⾏遍历,在此过程中进⾏添加、更新及移除等操作

  • ⽣成 Generate: 将变换后的 AST 再转换为 JS 代码, 使⽤到的模块是 babel-generator

image.png

如何写⼀个babel插件

Babel解析成AST,然后插件更改AST,最后由Babel输出代码

那么Babel的插件模块需要你暴露⼀个function,function内返回visitor

module.export = function(babel){ 
	return { 
		visitor:{ } 
	} 
}

visitor是对各类型的AST节点做处理的地⽅,那么我们怎么知道Babel⽣成了的AST有哪些节点呢?

案例

你的git⼯作流是怎样的

各个分支:

  • master: 主分⽀
  • dev: 主开发分⽀,包含确定即将发布的代码
  • feature: 新功能分⽀,⼀般⼀个新功能对应⼀个分⽀,对于功能的拆分需要⽐较合理,以避免⼀些后⾯不必要 的代码冲突
  • release: 发布分⽀,发布时候⽤的分⽀,⼀般测试时候发现的 bug 在这个分⽀进⾏修复
  • hotfix 分⽀,紧急修 bug 的时候⽤

lightMerge

可以自由合并各个开发或测试分支到目标分支,实现按需求部署。

rebase 与 merge的区别

git rebase 和 git merge ⼀样都是⽤于从⼀个分⽀获取并且合并到当前分⽀.

marge 特点:

  • ⾃动创建⼀个新的commit 如果合并的时候遇到冲突,仅需要修改后重新commit
  • 优点:记录了真实的commit情况,包括每个分⽀的详情
  • 缺点:因为每次merge会⾃动产⽣⼀个merge commit,所以在使⽤⼀些git 的GUI tools,特别是commit⽐较频繁 时,看到分⽀很杂乱。

rebase 特点:

  • 会合并之前的commit历史
  • 优点:得到更简洁的项⽬历史,去掉了merge commit
  • 缺点:如果合并出现代码问题不容易定位,因为re-write了history

因此,当需要保留详细的合并信息的时候建议使⽤git merge,特别是需要将分⽀合并进⼊master分⽀时;当发现⾃⼰修改某个功能时,频繁进⾏了git commit提交时,发现其实过多的提交信息没有必要时,可以尝试git rebase.

git reset、git revert 和 git checkout 有什么区别

  • git reset 可以将⼀个分⽀的末端指向之前的⼀个 commit。然后再下次 git 执⾏垃圾回收的时候,会把这个 commit 之后的 commit 都扔掉。
  • git reset 还⽀持三种标记,⽤来标记 reset 指令影响的范围: --mixed:会影响到暂存区和历史记录区。也是默认选项 --soft:只影响历史记录区 --hard:影响⼯作区、暂存区和历史记录区 注意:因为 git reset 是直接删除 commit 记录,从⽽会影响到其他开发⼈员的分⽀,所以不要在公共分⽀(⽐如 develop)做这个操作。
  • git checkout 可以将 HEAD 移到⼀个新的分⽀,并更新⼯作⽬录。因为可能会覆盖本地的修改,所以执⾏这个指令 之前,你需要 stash 或者 commit 暂存区和⼯作区的更改。

git revert 和 git reset 的⽬的是⼀样的,但是做法不同,它会以创建新的 commit 的⽅式来撤销 commit,这样能保 留之前的 commit 历史,⽐较安全。另外,同样因为可能会覆盖本地的修改,所以执⾏这个指令之前,你需要 stash 或者 commit 暂存区和⼯作区的更改。

什么是Code Splitting

在最开始使用Webpack的时候, 都是将所有的js文件全部打包到一个build.js文件中(文件名取决与在webpack.config.js文件中output.filename), 但是在大型项目中, build.js可能过大, 导致页面加载时间过长. 这个时候就需要code splitting, code splitting就是将文件分割成块(chunk)。

我们将代码进行 Code Spliting (代码分离),分离成很多块(chunk) ,然后可以按需要加载不同的代码,也就是 Lazy Loading (懒加载)。这样的话,可以极大的减少我们的初始化时的请求次数,而且还可以命中浏览器的缓存,避免多次请求相同代码,来提升网页加载的速度。

JS 代码分离的方式

1、使用 SplitChunksPlugin 插件(同步)

在配置文件中添加以下代码即可分离同步及异步代码。

// webpack.config.js

module.exports = {
  // ...
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
 // ...
}

这样将会新生成一个以 vendor开头的 chunk, 能够自动实现主代码和导入的第三方库的分离,而且还能去除掉重复依赖的模块。

使用 import() 函数动态导入(异步)

对于使用 import() 的异步代码,我们并不需要进行 optimization.splitChunks 的配置,打包后会自动分离到一个新的 chunk 中。

import('lodash').then(({default: _ }) => {
    console.log(_.join(['Axton', 'Tang'], ' '))
})

Preloading 和 Prefetching

  • prefetch(预获取):加载将来某些导航下可能需要的资源
  • preload(预加载):加载当前导航下可能需要资源, 会与主chunk 同步加载,不推荐使用。
btn.addEventListener('click', () => {
  import(/* webpackPrefetch: true */ 'lodash').then(({default: _ }) => {
    console.log(_.join(['Axton', 'Tang'], ' '))
  })
})
optimization: {
    splitChunks:{
        // all 对同步、异步代码都做代码分割
        // async 只对异步代码做代码分割
        // initial 只对同步代码做分割
        // 同步代码 inport loadsh from 'loadsh'
        // 异步代码 import('loadsh')
        
        chunks: 'all',
        cacheGroups: {
            // 第三方模块
            vendor: {
                name: 'vendor',
                // 对于像loadsh可能被判定为公共模块,也可能被判定为第三分模块,设置第三方模块的优先级高于公共模块,就可以被优先检测
                priority: 1,
                // 检测方法,通过是否来自node_modules来判断的
                test: /node_modules/,
                // 为了看到代码分割的效果,把值设置到最小
                minsize: 0,
                minChunks: 1
            },
            // 公共模块
            common: {
                // 每个组的名字
                name: 'common',
                // 优先级,优先级越高,越先检测处理, 
                priority: 0,
                // 实际开发中,可以写成5*1024,即5kb
                minsize: 0,
                // 检测模块被引用了几次
                // 对于第三方模块,引用1次就应该单独打包
                // 对于公共模块,引用2次以上就该单独打包
                minChunks: 2
            }
        }
    }
}