webpack知多少?

1,407 阅读12分钟

image.png

一、webpack是什么?

webpack 是一个现代 JavaScript 应用程序的静态模块打包器,能够以一种相对一致且开放的处理方式,加载应用中的所有资源文件(图片、CSS、视频、字体文件等),并将其合并打包成浏览器兼容的 Web 资源文件 image.png

二、webpack打包过程

  • 配参:从配置文件和Shell语句读取与合并参数,得出webpack的配置参数
    • 配置文件默认下为webpack.config.js(可以通过命令的形式指定配置文件)
    • webpack将各个配置项拷贝到options对象中,然后加载用户配置的plugins
    • 基本webpack.config.js如下
var path = require('path');
var node_modules = path.resolve(__dirname, 'node_modules');
var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js');
 
module.exports = {
  // 入口文件,是模块构建的起点
  entry: './path/to/my/entry/file.js',
  // 文件路径指向(可加快打包过程)。
  resolve: {
    alias: {
      'react': pathToReact
    }
  },
  // 生成文件,是模块构建的终点,包括输出文件与输出路径。
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: '[name].js'
  },
  // 这里配置了处理各模块的 loader
  module: {
    loaders: [
      {
        test: /.js$/,
        loader: 'babel',
        query: {
          presets: ['es2015', 'react']
        }
      }
    ],
    noParse: [pathToReact]
  },
  // webpack 各插件对象,在 webpack 的事件流中执行对应的方法。
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
};
  • 开始编译: 启动webpack,创建Compiler对象(通过上述参数初始化), 调用Compilerrun来真正启动webpack编译构建流程
    • Compiler对象是一个单局全例,负责把控整个webpack打包的构建流程,负责文件监听和启动编译
    • 每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程
    • compilation对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息
  • 确定入口从入口文件(entry)开始解析
    • entry可以有三种不同形式: string | object | array,分别对应: 一对一(一个入口一个打包文件),多对一(多个入口,一个打包文件),多对多(多个入口,多个打包文件)
  • 编译过程:对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件(编译),用loader对一个模块转换后,使用acorn解析转换后的内容,输出对应的抽象语法树(AST),从配置的入口模块开始,分析其AST,当遇到require等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系
        module.exports = {
          //...
          module: {
            rules: [
              {
                test: /.(le|c)ss$/,
                use: ['style-loader', 'css-loader', 'less-loader'],
                exclude: /node_modules/,
              },
            ],
          },
        };
        // 这样的话就是先处理less-loader,再css-loader,再style-loader
  • 输出资源根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk(配置在entry的模块,或者是动态引入的模块),再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 输出完成在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

上述过程简化一下:

  • 初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
  • 开始编译: 根据我们的webpack配置注册好对应的插件调用 compile.run 进入编译阶段,在编译的第一阶段是 compilation,他会注册好不同类型的module对应的 factory
  • 编译模块: 进入 make 阶段,会从 entry 开始进行两步操作: 第一步是调用 loaders 对模块的原始代码进行编译,转换成标准的JS代码, 第二步是调用 acorn 对JS代码进行语法分析,然后收集其中的依赖关系。每个模块都会记录自己的依赖关系,从而形成一颗关系树。
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的

三、常见loader

  • 样式方面
    • style-loader :将模块导出的内容作为样式并添加到 DOM 中
    • css-loader :加载 CSS 文件并解析 import 的 CSS 文件,最终返回 CSS 代码
    • less-loader :加载并编译 LESS 文件
    • sass-loader :加载并编译 SASS/SCSS 文件
    • postcss-loader :使用 PostCSS 加载并转换 CSS/SSS 文件
  • 框架
  • 检查语言
    • eslint-loader:通过 ESLint 检查 JavaScript 代码
    • tslint-loader:通过 TSLint 检查 TypeScript 代码
  • 语法转换
  • 其他loader
    • source-map-loader:加载额外的 Source Map 文件,以方便断点调试
    • file-loader: 可以解析项目中URL的引入,将文件拷贝到相应的路径,并修改打包后文件的引入路径,让其指向正确的文件
    • url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)
    • json-loader 加载 JSON 文件(默认包含)

详细可看官网: Loaders | webpack 中文文档 (docschina.org)

四、常见plugin

详细可看官网:Plugins | webpack 中文文档 (docschina.org)

五、plugin和loader的区别

  • 看完上面常见的loader和plugin,好像都是在webpack打包过程发挥一些作用,这是你应该就会产生一个疑问了:那他们的区别在哪里呢?

  • 第一个方面是运行时机和机制

    • 如上述打包过程可以看到,loader的运行时间是只编译时,同时loader的职责是单一的,每个loader只需要完成一种转换,多个loader转换同一个文件时,会链式顺序执行 (链式执行就是上一个loader的处理结果可以传给下一个loader接着处理,上一个Loader的参数options可以传递给下一个loader,直到最后一个loader,返回Webpack所期望的JavaScript)
    • plugin是贯穿整个webpack运行周期,在webpack运行的生命周期中会广播出许多事件,plugin会监听这些事件,然后再在核实的时机通过webpack提供的API改变输出结果
  • 第二个方面是功能

    • loader只专注于转化文件这个领域,对文件进行压缩,语言翻译(因为webpack只能理解JavaScript和JSON文件),最终一起打包到指定的文件中
    • webpack赋予了webpack各种灵活的功能,主要在于解决loader无法实现的其他事,例如打包优化,资源管理,环境遍历注入等
  • 第三个方面是本质

    • loader本质就是一个函数
    • plugin是插件
  • 第四个方面是在配置

    • loader在module.rules配置,类型为数组,每一项都是一个object,对其进行配置
    • plugin在plugin独立配置,类型为数组,每一项是一个plugin的实例,参数通过构造函数参数传入

几句话说不清楚如何配置,继续往下看看吧

六、loader的基本配置

  • 可以看到如下的配置项出现了test,use,exclude,来了解一下他们分别是什么吧
 module: {
            rules: [
              {
                //选中.less,.css结尾的文件
                test: /.(le|c)ss$/, 
                use: ['style-loader', 'css-loader', 'less-loader'],
                //不选中node_modules中的文件
                exclude: /node_modules/,
              },
            ],
          },
  • test(必须): 类型文件,作用就是筛选资源,符合条件的资源让这项规则中的loader处理

    • test是字符串时:为资源所在目录绝对路径或者资源的绝对路径
    {
         test: path.resolve(__dirname, 'src/css'),
         use: ['style-loader', 'css-loader', 'less-loader'],
         exclude: /node_modules/,
    },
    
    • test是函数时:接收的参数为资源的绝对路径,返回true表示该资源符合条件
    {
         test: function (path) { 
             return path.indexOf('.css') > -1
         },
         use: ['style-loader', 'css-loader', 'less-loader'],
         exclude: /node_modules/,
    },
    
    • test是数组时:数组每一项可以为字符串、正则表达式、函数,只要符合数组中任一项条件即可
    {
         test: [path.resolve(__dirname, 'src/css'), /.(le|c)ss$/],
         use: ['style-loader', 'css-loader', 'less-loader'],
         exclude: /node_modules/,
    },
    
  • use(必须):使用哪些loader处理符合条件的资源

    • use: ['style-loader']其实是use: [ { loader: 'style-loader'} ]的简写
    • 可以通过options传入loader,可以理解为loader的选项
    rules: [
        { 
            test: /\.css$/, 
            use: [ 'style-loader',
                   { loader: 'css-loader', 
                     options: {
                         importLoaders: 1 
                         }
                    },
                  ]
          },
     ],
    
    • 相同的功能也可以使用loader来配置: loader:'css-loader' 是 use:[{loader:'css-loader'}] 的简写
        rules: [
            { 
                test: /\.css$/, 
                loader:'css-loader',
              },
         ],
        
        ```
    
  • exclude: 它的作用就是将指定资源文件排除在外,就算被test选中也是无用的,用法跟test是一样的,就不一一分类赘述啦

{
         exclude: path.resolve(__dirname, 'node_modules'),  //字符 
         exclude: [/node_modules/ , path.resolve(__dirname, 'node_modules')], //数组
         exclude:function (content) { 
             return content.indexOf('node_modules') > -1 
         }, //函数
},

七、简单编写loader的思路

可以先看一下官网API: loader API | webpack 中文网 (webpackjs.com)

  • 官方:所谓 loader 只是一个导出为函数的 JavaScript 模块。loader runner 会调用这个函数,然后把上一个 loader 产生的结果或者资源文件(resource file)传入进去。函数的 this 上下文将由 webpack 填充,并且loader runner具有一些有用方法,可以使 loader 改变为异步调用方式,或者获取 query 参数

  • 从上我们可以获取到几个信息点

    • 首先它肯定是一个函数
    • 其次this上下文由webpack填充,那么不能写成箭头函数module.exports = () => {},而用声明式module.exports = function(){},否则this指向会有问题
    • this 是由 webpack 提供的对象,能够获取当前 loader 所需要的各种信息
    • 可以同步调用也可以异步调用
    //同步调用
    module.exports = function(content, map, meta) {
        //可以直接同步返回转换后的content
        return someSyncOperation(content);
        //也可以使用this.callback传递更多参数,更加灵活,此时return返回undefined
        this.callback(null, someSyncOperation(content), map, meta);
        return; 
    };
    
    //异步调用
    module.exports = function(content, map, meta) {
    //对于异步 loader,使用 this.async 来获取 callback 函数:
      var callback = this.async();
      someAsyncOperation(content, function(err, result) {
        if (err) return callback(err);
        callback(null, result, map, meta);
      });
    };
    
  • 在介绍loader的配置时我们还介绍了可以配置loader的options,那么在loader内部如何获取呢

    • 可以通过this.query 来获取这个 option 对象
    • 也可以通过loader-utils 中提供的 getOptions 方法 来提取给定 loader 的 option(推荐)
        const loaderUtils = require('loader-utils')
        module.exports = function (source){
          const options = loaderUtils.getOptions(this)
        }
    
  • 最后开发上需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事情

八、plugin的配置

  • 插件的使用很简单,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建它的一个实例
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装,用require引入
const webpack = require('webpack'); // 用于访问内置插件
const config = {
  module: {
    rules: [
      { test: /.txt$/, use: 'raw-loader' }
    ]
  },
  plugins: [
  //创建实例,通过option自定义传入参数
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
};

module.exports = config;

九、简单编写plugin的思路

老规矩,先上官网:Plugin API | webpack 中文网 (webpackjs.com)

  • plugin实际是一个类
  • compiler 暴露了和 Webpack 整个生命周期相关的钩子
  • compilation 暴露了与模块和依赖有关的粒度更小的事件钩子(ompilation 对象检测的是随时可变的项目文件,只要文件有改动,compilation就会重新创建)
  • 插件需要在其原型上绑定apply方法,才能访问 compiler 实例
  • 传给每个插件的 compiler 和 compilation对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件
  • 找出合适的事件点去完成想要的功能
    • emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(emit 事件是修改 Webpack 输出资源的最后时机)
    • watch-run 当依赖的文件发生变化时会触发
  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住

十、webpack的热更新

  • Webpack热更新( Hot Module Replacement,简称 HMR),无需完全刷新整个页面的同时,更新所有类型的模块,是 Webpack 提供的最有用的功能之一
  • 可以有两个方法来开启
    • 可以通过 --hot 开启
     "scripts": {
        "dev": "webpack-dev-server --open --port 3000  --hot"
      },
    
    • 可以通过 HotModuleReplacementPlugin开启
    const webpack = require('webpack'); //第一步
      devServer: {
            hot: true  //第二步,打开 webpack-dev-server 的热更新开关
      },
       plugins: [ 
            new webpack.HotModuleReplacementPlugin() //第三步,加入webpack自带插件 HotModuleReplacementPlugin
       ]
    
  • 主要是通过以下几种方式,来显著加快开发速度:
    • 保留在完全重新加载页面时丢失的应用程序状态
    • 只更新变更内容,以节省宝贵的开发时间
    • 调整样式更加快速 - 几乎相当于在浏览器调试器中更改样式
  • 实现原理:
    • Webpack 编译打包之后得到一个 Compilation ,并将Compilation传递到 Webpack-dev-middleware 插件中,Webpack-dev-middleware 可以通过 Compilation 调用 Webpack中 的 Watch 方法实时监控文件变化,并重新编译打包写入内存
      • 不直接生成的文件的原因在于访问内存中的代码比访问文件系统中的文件更快,同时减少了代码写入文件的开销
      • 归功于memory-fs(webpack-dev-middleware的依赖库),将 webpack 原本的 outputFileSystem 替换成了MemoryFileSystem 实例
          // webpack-dev-middleware/lib/Shared.js
        var isMemoryFs = !compiler.compilers && compiler.outputFileSystem instanceof MemoryFileSystem;
          if(isMemoryFs) {
              fs = compiler.outputFileSystem;
          } else {
              fs = compiler.outputFileSystem = new MemoryFileSystem();
          }
      
      
    • 浏览器加载页面后,webpack-dev-server在浏览器和服务器之间建立一个WebSocket长连接
      • 以便将 webpack 编译和打包的各个阶段状态告知浏览器
    • Webpack 监听到文件变化后,增量构建发生变更的模块,并通过 WebSocket 发送 hash 事件
      • webpack-dev-server 调用 webpack api 监听 compile的 done 事件,当compile 完成后,webpack-dev-server通过 _sendStatus 方法将编译打包后的新模块 hash 值发送到浏览器端
      // webpack-dev-server/lib/Server.js
      compiler.plugin('done', (stats) => {
        // stats.hash 是最新打包文件的 hash 值
        this._sendStats(this.sockets, stats.toJson(clientStats));
        this._stats = stats;
      });
      
    • dev-server监听webpackHotUpdate消息调用HMR runtime中的 check 方法,检测是否有新的更新
      • 在check过程会利用JsonpMainTemplate.runtimehotDownloadManifest向server端发送Ajax请求,返回的JSON包含了所有要更新的模块的hash---返回hash值
      • 会利用JsonpMainTemplate.runtimehotDownloadUpdateChunk通过 jsonp 请求最新的模块代码,然后将代码返回给 HMR runtime---返回hash值对应的代码块
    • HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用
    • 如果在热更新过程中出现错误,热更新将回退到刷新浏览器

以上过程简单来说就是: 首先是建立起浏览器端和服务器端之间的通信,浏览器会接收服务器端推送的消息,如果需要热更新,浏览器发起http请求去服务器端获取打包好的资源解析并局部刷新页面

  • 业界许多 Webpack Loader 已经提供了针对不同资源的 HMR 功能,例如:
    • style-loader 内置 Css 模块热更
    • vue-loader 内置 Vue 模块热更

十一、什么是sourceMap

  • sourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中debug问题会带来非常糟糕的体验
  • sourceMap可以帮助我们快速定位到源代码的位置,提高我们的开发效率