webpack介绍

138 阅读5分钟

模块打包工具

模块打包工具主要工作方式主要分成两种:

  • 讲存在依赖关系的模块按照特定规则合并为单个JS 文件,一次全部加载进页面。
  • 在页面初始的时候加载一个入口模块,其他模块异步进行加载。

Webpack-server

webpack能够实现热更背后的原理是webpack-server。它可以看做一个服务器,每次都接受浏览器的请求,然后返回需要的数据。但是需要注意的是webpack-sever在运行的时候不是真的将打包结果写入bundle.js,而是写入内存中,每次收到请求的时候就把内存中的数据返回给webpack。

webpack打包流程

打包过程其实就是对依赖关系的一种解析过程。

在一切流程最开始,我们需要指定一个或者多个入口(entry),也就是告诉webpack具体从源码的哪个位置开始打包,如果把工程的各个模块的依赖关系当做一棵树,那么入口就是这颗依赖树的树根。

chunk被称为代码块,里面包含着很多依赖关系。根据配置不同,一个工程在打包的时候可能会产生一个或多个chunk。由这个chunk打包之后的最终产物,我们称之为bundle。

入口和出口

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

module.exports = {
  entry: ['babel-polyfill', './src/index.js'],
  output: { filename: 'bundle.js' },
  mode: 'development',
  plugins: [new htmlPlugin({ title: path.basename(__dirname) })],
  devServer: {
    publicPath: '/dist/',
    port: 3000,
  },
};

entry传入一个数组的作用是将多个资源预先合并。我们也可以在entry处传入一个函数便于动态控制。对于多页应用,我们也可以设置多个入口。

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

module.exports = {
  entry: () => ({
    partA: './src/partA.js',
    partB: './src/partB.js',
    index: ['babel-polyfill', './src/index.js'],
  }),
  // filename指输出资源名称
  output: { filename: '[name].js' },
  mode: 'development',
  plugins: [new htmlPlugin({ title: path.basename(__dirname) })],
  devServer: {
    publicPath: '/dist/',
    port: 3000,
  },
};

loader

loader在webpack中称之为预处理器。我们一般写页面的时候有对应的js文件和css文件,但是导入的时候却只用导入js文件不用导入css文件,这是为什么呢,背后原理是loader帮我们做了一些数据的预处理。

预处理器有一种函数式编程的思想,就是采用链式的管道思维。output = loader(input),可以多个loader组合然后工作。

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

module.exports = {
  entry: './src/index.js',
  output: {
    filename: '[name].js',
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      }
    ],
  },
  plugins: [new htmlPlugin({ title: path.basename(__dirname) })],
  devServer: {
    publicPath: '/dist/',
    port: 3000,
  },
};

代码分片

通过代码分片可以将代码进行拆分,实现按需加载,而不必一次全部加载。

通过入口划分代码

最容易想到的代码分片就是通过entry来划分代码。另外还可以将多个页面都需要使用到的依赖比如react划分到一个单独的entry,这样就可以利用客户端缓存,不需要每次请求页面的时候都重新加载。

其他分片方案

其他分片方案就是使用插件,一般思路就是将常用依赖打包到common.js中,利用缓存来提高加载速度。

Plugin

插件相比于loader,具有更强大的处理能力。plugin用于接受一个插件数组。一般都是和loader结合起来一起使用。

缓存问题

代码分片利用缓存但是也可能会带来缓存的问题。如果开发者想对某处代码进行bug fix,并希望立即更新到所有的浏览器中,而不希望他们使用旧的缓存。此时最好的办法是改变资源url,这样可以迫使客户端下载最新的资源。其中一种做法是可以对资环的hash做更改。

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle@[chunkhash].js',
  },
  mode: 'production',
  devServer: {
    publicPath: '/dist/',
    port: 3000,
  },
};

这样通过每次打包都会对资源内容做一次hash,并作为版本号放在文件名中,当代码发生变化的时候hash值也会发生变化。

其他插件可以解决缓存中的其他问题。

Tree Shaking

tree shaking就是将写入但是没有实际运行的代码在打包的时候剔除掉。但是tree shaking只能是在ES 6有效而对CommonJS无效。

原因在于CommonJS对模块依赖解析是动态的,而ES6是静态的。动态是指,模块依赖关系的建立是发生在代码运行阶段。静态是指,模块依赖关系发生在代码编译阶段。

webpack会将那些没有被执行到的代码进行标记,并在资源压缩时将他们从最终bundle中去除掉。

Happy Pack

打包过程的主要流程分为如下4步:

  1. 从配置中获取打包入口
  2. 匹配loader规则,并对入口模块进行转译
  3. 对转义后的模块进行依赖查找(如a.js中加载b.js)
  4. 对新找到的模块重复步骤2和3,直到没有新模块依赖

不难看出步骤2到4是一个递归的过程。webpack是单线程找,happypack可以实现多线程找。

热更

热更称之为Hot Module Replacement(HMR)。HMR的核心就是客户端从服务端拉取更新后的资源(不是整个资源,而是chunk diff)。

参考资料

《Webpack实战 入门、进阶与调优》