Skip to content

webpack核心 ​

为什么需要webpack ​

模块化的利与弊 ​

优点:

  • 代码复用
  • 开发的时候效率提升 缺点:
  • 兼容性问题:目前浏览器仅支持ES6模块
  • 性能问题:划分过多的js文件,浏览器加载速度变慢
  • 工具问题:浏览器不支持npm下载的第三方包

上面提到的问题,为什么在node端没有那么明显,反而到了浏览器端变得如此严重呢? 在node端,运行的JS文件在本地,因此可以本地读取文件,它的效率比浏览器远程传输文件高的多

根本原因 ​

在浏览器端,开发时态(devtime)和运行时态(runtime)的侧重点不一样

开发时态,devtime:

  1. 模块划分越细越好
  2. 支持多种模块化标准
  3. 支持npm或其他包管理器下载的模块
  4. 能够解决其他工程化的问题

运行时态,runtime:

  1. 文件越少越好
  2. 文件体积越小越好
  3. 代码内容越乱越好
  4. 所有浏览器都要兼容
  5. 能够解决其他运行时的问题,主要是执行效率问题

webpack应运而生 ​

我们需要一个工具(构建工具),能够将开发时态的代码转换成运行时态的代码,所以webpack应运而生,注意两个点:

  1. 构建工具是在node环境运行的
  2. 打包后的代码不存在模块化的概念,就是一个函数,一个js文件

webpack的安装 ​

安装 ​

shell
npm install webpack --save-dev
  • webpack:核心包,包含了webpack构建过程中要用到的所有api
  • webpack-cli:提供一个简单的cli命令,它调用了webpack核心包的api,来完成构建过程

使用 ​

shell
npx webpack

模块化兼容性 ​

不同模块化标准导入导出,webpack打包之后的处理结果如下

  • 使用es6导出,commonjs导入会得到一个包含default属性的对象

  • 使用commonjs导出,es6导入的时候需要使用导入默认值,或者全部导入

编译过程 ​

初始化 ​

  • 读取配置:webpack会依托一个第三方库yargs将CLI参数、配置文件、默认配置进行融合,形成一个最终的配置对象。

  • 初始化插件:实例化并调用配置中指定的插件。

  • 构建编译对象:创建一个新的Compiler对象,并注册所有插件监听的钩子。

  • 确定入口:根据entry配置确定一个或多个编译入口点。

编译 ​

  1. 创建chunk

chunk是webpack在内部构建过程中的一个概念,译为块,它表示通过某个入口找到的所有依赖的统称。

根据入口模块(默认为./src/index.js)创建一个chunk

chunk 有两种形式:

  • initial(初始化) 是入口起点的 main chunk。此 chunk 包含为入口起点指定的所有模块及其依赖项。
  • non-initial 是可以延迟加载的块。可能会出现在使用 动态导入(dynamic imports) 或者 SplitChunksPlugin 时。
js
  // webpack.config.js
  module.exports = {
    entry: './src/index.jsx',
  };

  // ./src/index.jsx
  import React from 'react';
  import ReactDOM from 'react-dom';

  import('./app.jsx').then((App) => {
    ReactDOM.render(<App />, root);
  });

  //Output:
  /dist/main.js - 一个 initial chunk 包含:
    - ./src/index.jsx
    - react
    - react-dom
  /dist/394.js - non-initial chunk

默认情况下,这些 non-initial chunk 没有名称,因此会使用唯一 ID 来替代名称。 在使用动态导入时,我们可以通过使用 magic comment(魔术注释) 来显式指定 chunk 名称:

js
  import(
    /* webpackChunkName: "app" */
    './app.jsx'
  ).then((App) => {
    ReactDOM.render(<App />, root);
  });

    //Output:
    /dist/main.js - 一个 initial chunk
    /dist/app.js - non-initial chunk

每个chunk都有至少两个属性:

  • name:默认为main
  • id:唯一编号,开发环境和name相同,生产环境是一个数字,从0开始
  1. 构建所有依赖模块

  1. 产生chunk assets

在第二步完成后,chunk中会产生一个模块列表,列表中包含了模块id和模块转换后的代码(require-->webpack_require,路径变换...)

接下来,webpack会根据配置为chunk生成一个资源列表,即chunk assets,资源列表可以理解为是生成到最终文件的文件名和文件内容

chunk hash是根据所有chunk assets的内容生成的一个hash字符串

hash:一种算法,具体有很多分类,特点是将一个任意长度的字符串转换为一个固定长度的字符串,而且可以保证原始内容不变,产生的hash字符串就不变

  1. 合并chunk assets

将多个chunk的assets合并到一起,并产生一个总的hash

输出 ​

  • 文件生成:Webpack将chunk转换成文件,根据output配置确定输出的文件名称和路径。
  • 代码优化:在chunks生成文件之前,可能会有压缩和优化的过程,此时会运行Optimization相关的插件。
  • 生成浏览器可执行文件:输出最终构建的文件至指定目录,这通常是dist文件夹。

手写编译结果 ​

手写一个webpack打包结果

js
//不能造成全局变量污染,所以需要创建一个闭包
(function (modules) {
  var installedModules = {} //用于缓存模块的导出结果


  function __webpack_require__(moudleId){//moudleId就是模块的路径

    if(installedModules[moudleId]){ //检查缓存
      return installedModules[moudleId]
    }

    var module = {
      exports:{}
    }
    modules[moudleId].call(module.exports,module,module.exports,__webpack_require__) //运行模块  this指向module.exports
    var result = module.exports //得到模块导出的结果
    installedModules[moudleId] = result
    return result
  }

  //执行入口模块
  return __webpack_require__("./src/index.js")  //require函数相当于是运行一个模块,得到模块导出结果

})({ 

  //该对象保存了所有的模块,以及模块对应的代码
  //使用eval有助于提高调试体验和减少源映射文件的大小
  // //# sourceURL=webpack:///./src/a.js  注释可以保证调试的时候浏览器找到对应的js文件

    "./src/a.js": function (module, exports) {
        eval("console.log(\"module a\")\nmodule.exports = \"a\";\n //# sourceURL=webpack:///./src/a.js")
    },
    "./src/index.js": function (module, exports, __webpack_require__) {
        eval("console.log(\"index module\")\nvar a = __webpack_require(\"./src/a.js\")\na.abc();\nconsole.log(a)\n //# sourceURL=webpack:///./src/index.js")
    }
})

配置文件 ​

webpack提供的cli支持很多的参数,例如--mode,但更多的时候,我们会使用更加灵活的配置文件来控制webpack的行为

默认情况下,webpack会读取webpack.config.js文件作为配置文件(这也是必须使用commonjs格式的原因),但也可以通过CLI参数npx webpack --config custom.config.js来指定某个配置文件

配置文件中通过CommonJS模块导出一个对象,对象中的各种属性对应不同的webpack配置

当命令行参数与配置文件中的配置出现冲突时,以命令行参数为准。

注意点:

  1. 配置文件中的代码,必须是有效的node代码,这里是需要在编译过程中运行的
  2. 我们自己写的代码在编译过程中,webpack只会把他当成文件来分析其中的模块依赖关系,而不会运行里面的代码

基本配置:

  1. mode:编译模式,字符串,取值为development或production,指定编译结果代码运行的环境,会影响webpack对编译结果代码格式的处理
  2. entry:入口,字符串,指定入口文件
  • 关于路径的一些前置知识:
    1. ./ :
      • 模块化代码中,比如require(“./”),表示当前js所在的目录
      • 在路径处理中,"./"表示node运行目录
    2. __dirname:
      • 所有情况下,都表示当前运行的js文件所在的目录,他是一个绝对路径
  • path模块:
    • 根据多段路径拼接出一个绝对路径 path.resolve(__dirname,"./dist")
    • 不同操作系统的路径分隔符不一样,所以需要使用path模块来处理路径
js
  module.exports = {
    //单个入口语法
    entry: "./src/index.js",  //简写
    entry: {
      main: "./src/index.js"
    }    

    //一次注入多个依赖文件,并且将它们的依赖关系绘制在一个 "chunk" 中时
    entry: ['./src/file_1.js', './src/file_2.js'],

    //对象语法
    entry: {
      app: './src/app.js',
      adminApp: './src/adminApp.js',
    },
  }
  1. output:出口,对象,指定编译结果文件
  • name:chunkname,默认为main
  • hash: hash,chunkhash,通常用于解决缓存问题
js
  module.exports = {
    mode: "development",
    entry: {
      app: './src/app.js',
      search: './src/search.js',
    },
    output: {
      path: path.resolve(__dirname, "./dist"),
      filename: '[name].[contenthash:5].js',
    }    
  }

入口出口配置最佳实践 ​

  1. 应用程序和第三方库分开打包 如果要将应用程序的代码和第三方库的代码分开打包,可以使用多入口或利用SplitChunksPlugin。
js
// webpack.config.js
module.exports = {
  entry: {
    app: './src/index.js',
    vendor: ['lodash', 'jquery'],
  },
  output: {
    filename: '[name].bundle.js',
    path: __dirname + '/dist',
  },
};

使用SplitChunksPlugin:

js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
    },
  },
};

项目目录结构:

plaintext
root/
|- src/
|  |- index.js
|- dist/
|- webpack.config.js
  1. 多页应用(MPA)
js
module.exports = {
    entry:{
        pageA: "./src/pageA/index.js", 
        pageB: "./src/pageB/index.js", 
        statistics: "./src/statistics/index.js"
    },
    output:{
        filename:"[name].[chunkhash:5].js"
    }
}

项目目录结构:

plaintext
root/
|- src/
|  |- pageOne.js
|  |- pageTwo.js
|  |- ...
|- dist/
|- webpack.config.js
  1. 单页应用(SPA)
js
module.exports = {
    entry: "./src/index.js",
    output:{
        filename:"index.[hash:5].js"
    }
}

项目目录结构:

plaintext
root/
|- src/
|  |- index.js
|- dist/
|- webpack.config.js
  1. devtool:调试工具,字符串,指定调试工具
  • 开发环境最佳实践:eval 、 eval-source-map 、eval-cheap-source-map、eval-cheap-module-source-map
  • 生产环境最佳实践: none(不配置devtool)、source-map

loader ​

webpack loader: loader本质上是一个函数,它的作用是将某个源码字符串转换成另一个源码字符串返回。

js
module.exports = {
    module: { //针对模块的配置,目前版本只有两个配置,rules、noParse
        rules: [ //模块匹配规则,可以存在多个规则
            { //每个规则是一个对象
                test: /\.js$/, //匹配的模块正则
                use: [ //匹配到后应用的规则模块
                    {  //其中一个规则
                        loader: "模块路径", //loader模块的路径,该字符串会被放置到require中
                        options: { //向对应loader传递的额外参数

                        }
                    }
                ]
                //use: ["模块路径1", "模块路径2"]//简化版
            }
        ]
    }
}

plugin ​

webpack 插件是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在 整个 编译生命周期都可以访问 compiler 对象。

js
class MyPlugin{
    apply(compiler){
        compiler.hooks.事件名称.事件类型(name, function(compilation){
            //事件处理函数
        })
    }
}

要将插件应用到webpack,需要把插件对象配置到webpack的plugins数组中,如下:

js
module.exports = {
    plugins:[
        new MyPlugin()
    ]
}

webpack源码 ​

  • babel 的原理

    1. parse: 把代码 code 变成 AST
    2. traverse: 遍历 AST 进行修改
    3. generate: 把 AST 变成代码 code2 即 code --(1)-> ast --(2)-> ast2 --(3)-> code2
  • 为什么必须使用AST

    • 你很难用正则表达式来替换,正则很容易把 let a = 'let' 变成 var a = 'var'
    • 你需要识别每个单词的意思,才能做到只修改用于声明变量的 let
    • 而 AST 能明确地告诉你每个 let 的意思