webpack相关知识点、模块化

832 阅读5分钟

前端模块化

模块化的概念

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
  • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

模块化的好处

当引入多个

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

模块化规范

1. CommonJS

Node 应用由模块组成,采用 CommonJS 模块规范。CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值

  • 基本语法
    • 暴露模块:module.exports = valueexports.xxx = value
    • 引入模块:require(xxx),如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径

2. AMD

AMD规范则是异步加载模块,允许指定回调函数。如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范

前端模块化详解(完整版)

3. CMD

CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。

4. ES6模块化

export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

ES6 模块与 CommonJS 模块的差异

它们有两个重大差异:

① CommonJS 模块输出的是一个值的拷贝,赋值过程,ES6 模块输出的是值的引用,解构过程

② CommonJS(require) 模块是运行时加载,ES6 模块是编译时输出接口

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

webpack的作用是什么?

  • 模块打包
    • 根据依赖图的依赖关系,将不同模块的文件打包整合在一起,并保证它们之间的引用正确,执行有序。
  • 编译兼容
    • webpack的loader机制可以编译转换.less, .vue, .jsx等浏览器无法识别的格式文件,提高开发效率
  • 能力扩展
    • webpack的Plugin机制,在实现模块化打包和编译兼容的基础上,通过按需加载来拓展解决loader无法实现的其他事,比如实际的监听等。

webpack的几个关键概念

  • Entry:入口文件, 可以是一个或多个入口文件,指示webpack应该使用哪些模块作为构建依赖图的开始
  • Output:输出文件,告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件
  • Loaders:加载器,本质上是函数,接收资源文件进行转换,并返回新的文件
  • Plugins:插件,扩展功能,实现事件监听等
  • Mode:模式,通过选择 developmentproduction 或 none 之中的一个,来设置环境

模块化打包运行的流程?

问:webpack是如何把这些模块合并到一起,并保证正常工作的? 首先需要了解webpack的打包流程:

  • 初始化参数
    • 读取webpack的配置参数;
  • 开始编译
    • 启动webpack,创建Compiler对象并开始解析项目;
  • 确定入口
    • 从入口文件entry开始解析,构建其导入文件的依赖图,递归分析形成依赖关系树;
  • 编译模板
    • 对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件;
  • 完成模板编译并输出
    • 根据入口文件之间的依赖关系,形成一个个代码块chunk,将形成的代码块 chunk 输出到文件系统

:整个打包过程中通过plugin插件监听事件节点,执行插件任务进而达到干预输出结果的目的。

webpack源码中,文件的解析与构建主要依赖于compilercompilation两个核心对象实现。

每个模块间的依赖关系,都依赖于 AST 语法树,通过语法树可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。最终Webpack打包出来的bundle文件是一个IIFE的执行函数

webpack如何实现动态加载

在单页应用中,经常使用 webpack 的 动态导入 功能来异步加载模块,从而减少部分文件的体积。我们可以通过webpack 提供的 import() 和 require.ensure 两个 API 来使用该功能。

当文件较多时,用到 Webpack 中的 require.context() 方法,动态加载某个文件夹下的所有JS文件。语法如下:

require.context(directory, useSubdirectories = false, regExp = /^.//);
// 文件夹,是否包含子目录(true/false),正则匹配哪些文件

// 举例:获取stores文件夹下所有js文件,动态导入,不用一个个引入
let requireContext = require.context('./stores', true, /^./.*/index.js$/)

更多可以了解这篇:webpack是如何实现动态导入的

complier 和 compilation 区别

compiler对象是一个全局单例对象,他负责把控整个webpack打包的构建流程。complier 对象暴露了 webpack 整一个生命周期相关的钩子,是 webpack 初始化的参数的产物,包含options, entry, plugins等属性可以简单的理解为webpack的一个实例。

compilation对象是每一次构建的上下文对象,是 complier 的实例,是每一次 webpack 构建过程中的生命周期对象。它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程。

总结:两个对象都有自己的生命周期钩子。compiler对象是整个Webpack从启动到关闭的生命周期钩子的对象。compilation 对象 负责的是粒度更小的生命周期钩子,只是代表了一次新的编译。

是否写过Loader?简述一下编写loader的思路?

loader的配置信息

在 webpack 中 loader是一个函数,对匹配到的内容进行转换,将转换后的结果返回。通过配置可以看出,loader是支持以数组的形式配置多个的,loader支持链式的调用,执行顺序是从下到上,从右到左。为了保证loader能够正常工作,开发需要遵循一些规范,比如返回值必须是标准的JS代码字符串;遵循"单一职责"(即一个Loader只需要完成一种转换),只关心loader的输出以及对应的输出。

module.exports = {
  module: {
    rules: [
      { test: /.css$/, use:['style-loader', 'css-loader'] },
      // 从右到左,`css-loader`处理后的文件返回给`style-loader`处理后返回
      { test: /.ts$/, use: 'ts-loader' },
    ],
  },
};

总结编写loader的思路:

loader 的本质为函数,loader函数中的this上下文由webpack提供,因此我们不能将 loader设为一个箭头函数,可以通过this对象提供的相关属性,获取当前loader需要的各种信息数据。loader函数的工作就是获得处理前的原内容,对原内容执行处理后,返回处理后的内容。
详细可看参考文档:Webpack原理—编写Loader和Plugin

常见的loader

  • 样式类的 loader:css-loader, style-loader, less-loader
  • 文件类的 loader:url-loader, file-loader等。
  • 编译类的 loader:babel-loader, ts-loader
  • 校验测试类 loader:eslint-loader, jslint-loader

是否写过Plugin?简述一下编写Plugin的思路?

plugin的配置信息

plugins 是一个类,webpack 为 plugin 提供了很多内置的api,在Webpack运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果。需要在原型上定义 apply(compliers) 函数。同时指定要挂载的 webpack 钩子。

最基础的Plugin的代码是这样的:

class MyPlugin {
    // 在构造函数中获取用户给该插件传入的配置
    constructor(params){
    }
    // webpack初始化参数后会调用MyPlugin实例的apply方法,给插件传入complier对象。
    apply(complier){
         // 绑定钩子事件
        // complier.hooks.emit.tapAsync()
        compiler.plugin('emit', compilation => {
        })
    }
}
module.export = MyPlugin  // 导出 Plugin

在使用这个Plugin时,相关配置代码如下:

const MyPlugin = require('./MyPlugin.js');
module.export = {
  plugins:[
    new MyPlugin(options),
  ]
}

Webpack启动后,在读取配置的过程中会先执行new MyPlugin(options)初始化一个MyPlugin获得其实例。 在初始化compiler对象后,再调用实例方法myPlugin.apply(compiler)给插件实例传入compiler对象。 插件实例在获取到compiler对象后,就可以通过compiler.plugin(事件名称, 回调函数)监听到Webpack广播出来的事件。 并且可以通过compiler对象去操作Webpack。

总结编写Plugin的思路:

  1. 编写一个 JavaScript 命名函数。
  2. 在它的原型上定义一个 apply 方法。
  3. 在应用方法 apply() 中指定挂载的 webpack 事件钩子complier.hooks.
  4. 处理 webpack 内部实例的特定数据。
  5. 功能完成后调用 webpack 提供的回调。

常见的Plugin

  • html-webpack-plugin 会在打包后自动生成一个 html 文件,并且会将打包后的 js 文件引入到html 文件内。
  • optimize-css-assets-webpack-plugin 对 CSS 代码进行压缩。
  • mini-css-extract-plugin。将写入 style 标签内的 css 抽离成一个 用 link 导入 生成的 CSS 文件
  • webpack-parallel-uglify-plugin。开启多进程执行代码压缩,提高打包的速度。
  • clean-webpack-plugin。每次打包前都将旧生成的文件删除。
  • serviceworker-webpack-plugin。为网页应用增加离线缓存功能。

Loader和Plugin的区别?

  • loder是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,运行在打包文件之前,最终将资源打包到指定文件中
  • plugin赋予了webpack更多灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决和扩展了loder无法实现的其他功能,在整个编译周期中都起作用

webpack打包的整个运行时机如下图: image.png

在Webpack 运行的生命周期中会广播出许多事件,plugin 可以监听这些事件,在合适的时机通过Webpack提供的 API 改变输出结果。

对于loader,实质是一个转换器,将A文件进行编译形成B文件,操作的是文件,比如将A.scss或A.less转变为B.css,单纯的文件转换过程。

参考文档:loader和Plugin的区别

谈谈webpack与babel

webpack

webpack是一个模块化打包工具,打包js文件,css文件,图片,html等等,它可以分析整个项目的文件结构,确认文件之间的依赖,比如一个js文件引入了另一个js文件。在这个过程中可以合成js,压缩,最终生成项目文件。

babel

babel是一个JS编译器,用来转换将最新的JS语法转化成ES5语法,从而能够在大部分浏览器中运行。像ES6的箭头函数就可以做转换。babel执行过程中,分为三步:先解析(parsing)、再转化(TransForm)、最后生成(Generate)es5代码

image.png

// Babel 输入: ES2015 箭头函数 
[1, 2, 3].map((n) => n + 1); 
// Babel 输出: ES5 语法实现的同等功能 
[1, 2, 3].map(function(n) { return n + 1; });

但babel只转换语法的话,一些最新的api是不转化的,比如Object.assign, Promise等。所以babel还提供了很多插件,也就是babel-pilofill。安装后,即可支持浏览器运行。babel-pilofill基于core-js和regenerator。但pilofill是引入全部的api支持,如果只用了部分api,可以只引入相应的模块。

更多可以看这篇:深入浅出babel

babel还可以转换JSX语法,对React支持比较好。babel的presents,presents是指plugins的合集。Babel 一般需要配合 Webpack 来编译模块语法。
webpack中配置babel可以看这篇:webpack怎么配置babel?

plugins的执行过程是先出现先运行。

{
  "plugins": [
    "transform-decorators-legacy",  // 先运行
    "transform-class-properties"    // 再运行
  ]
}

但presents是先出现后执行,为了更好的兼容性。

{
  "presets": [
    "es2015",  // 最后运行es2015
    "react",
    "stage-2"  // 先运行stage-2
  ]
}

webpack热更新

模块热替换HMR - Hot Module Replacement)是 webpack 提供的最有用的功能之一。所谓的热替换是指在不需进行刷新重新加载页面的情况下,进行模块的替换,添加,删除等操作。
如果没有配置 HMR,那么每次改动时都需要刷新页面,才能看到改动之后的结果,对于调试来说,非常麻烦,而且效率不高,最关键的是,你在界面上修改的数据,会随着刷新页面会丢失。而 Webpack 热更新的机制存在,就算修改了代码,也不会导致刷新,而是保留现有的数据状态,只将模块进行更新替换。也就是说,既保留了现有的数据状态,又能看到代码修改后的变化。

其思路主要有以下几个方面:

  • 加载页面时保存应用程序状态
  • 只更新改变的内容,节省开发和调试时间
  • 修改样式更快,几乎等同于在浏览器中更改样式

一个带有热替换功能的webpack.config.js 文件的配置如下,做了这么几件事

  1. 引入了webpack库,安装依赖$ npm install webpack webpack-dev-server --save-dev
  2. 配置devServer,选项中的hot字段为true,代表开启热更新
    devServer: {
        contentBase: path.resolve(__dirname, 'dist'),
        hot: true,
        compress: true  // 表示父所有服务启用gzip压缩
    },
    
  3. 使用new webpack.HotModuleReplacementPlugin()
    plugins: {
        HotModuleReplacementPlugin: new webpack.HotModuleReplacementPlugin()
    },
    

参考文档:Webpack 如何实现热更新?