关于webpack,你想知道的全在这里了(包含运行机制常用配置以及v4 v5版本对比)

webpack初探

是什么

WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。

为什么要使用webpack

大型前端项目有着复杂的JavaScript代码和一大堆依赖包。为了简化开发的复杂度,前端社区涌现出了很多好的实践方法

  • 模块化,让我们可以把复杂的程序细化为小的文件;
  • 类似于TypeScript这种在JavaScript基础上拓展的开发语言:使我们能够实现目前版本的JavaScript不能直接使用的特性,并且之后还能转换为JavaScript文件使浏览器可以识别;
  • Scss,less等CSS预处理器

但是利用它们开发的文件往往需要进行额外的处理才能让浏览器识别,而手动处理又是非常繁琐的,这就为WebPack类的工具的出现提供了需求
在这里插入图片描述

主要功能

代码转换: TypeScript 编译成 JavaScript、SCSS,LESS 编译成 CSS.

文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片。

代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。

模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。

自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。

核心概念

Entry

入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。

进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。

每个依赖项随即被处理,最后输出到称之为 bundle 的文件中。

Output

output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。

基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。

Module

模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。

Chunk

代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。

Loader

loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。

loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。

本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。

Plugin

loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。

插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。

webpack运行流程

在这里插入图片描述

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
  3. 确定入口:根据配置中的 entry 找出所有的入口文件。
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
  5. 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

webpack简易实现

1.解析入口文件,获取AST

这里使用@babel/parser,这是 babel7 的工具,来帮助我们分析内部的语法,包括 es6,返回一个 AST 抽象语法树

const fs = require('fs')
const parser = require('@babel/parser')
const options = require('./webpack.config')
const Parser = {
  getAst: path => {
    // 读取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 将文件内容转为AST抽象语法树
    return parser.parse(content, {
      sourceType: 'module'
    })
  }
}
// 定义Compiler类
class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模块
    this.modules = []
  }
  // 构建启动
  run() {
    const ast = Parser.getAst(this.entry)
  }
  // 重写 require函数,输出bundle
  generate() {}
}
new Compiler(options).run()
2.遍历找到所有的依赖模块并转换为code

Babel 提供了@babel/traverse(遍历)方法维护这 AST 树的整体状态,这里使用它来找出依赖模块

使用@babel/core 和 @babel/preset-env将AST 语法树转换为浏览器可执行代码

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
const Parser = {
  getAst: path => {
    // 读取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 将文件内容转为AST抽象语法树
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
// ——————————————这里是新增代码————————————————————————————————————————
 getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
  getCode: ast => {
    // AST转换为code
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })
    return code
  }
}
  // ——————————————这里是新增代码————————————————————————————————————————
class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模块
    this.modules = []
  }
  // 构建启动
  run() {
    const { getAst, getDependecies, getCode } = Parser
    const ast = getAst(this.entry)
    const dependecies = getDependecies(ast, this.entry)
    const code = getCode(ast)
  }
  // 重写 require函数,输出bundle
  generate() {}
}
new Compiler(options).run()
3.递归解析所有依赖项,生成依赖关系图
const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
const Parser = {
  getAst: path => {
    // 读取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 将文件内容转为AST抽象语法树
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍历所有的 import 模块,存入dependecies
    traverse(ast, {
      // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 保存依赖模块路径,之后生成依赖关系图需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  },
  getCode: ast => {
    // AST转换为code
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })
    return code
  }
}
class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模块
    this.modules = []
  }
  // 构建启动
  run() {
    // 解析入口文件
    const info = this.build(this.entry)
    this.modules.push(info)
    this.modules.forEach(({ dependecies }) => {
      // 判断如果有依赖对象,递归解析所有依赖项
      if (dependecies) {
        for (const dependency in dependecies) {
          this.modules.push(this.build(dependecies[dependency]))
        }
      }
    })
//--------------这是新增代码----------------------------------------
   // 生成依赖关系图
    const dependencyGraph = this.modules.reduce(
      (graph, item) => ({
        ...graph,
        // 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
        [item.filename]: {
          dependecies: item.dependecies,
          code: item.code
        }
      }),
      {}
    )
  }
  build(filename) {
    const { getAst, getDependecies, getCode } = Parser
    const ast = getAst(filename)
    const dependecies = getDependecies(ast, filename)
    const code = getCode(ast)
    return {
      // 文件路径,可以作为每个模块的唯一标识符
      filename,
      // 依赖对象,保存着依赖模块路径
      dependecies,
      // 文件内容
      code
    }
  }
//--------------这是新增代码----------------------------------------  
  // 重写 require函数,输出bundle
  generate() {}
}
new Compiler(options).run()
4.重写 require 函数,输出 bundle
const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
const Parser = {
  getAst: path => {
    // 读取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 将文件内容转为AST抽象语法树
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍历所有的 import 模块,存入dependecies
    traverse(ast, {
      // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 保存依赖模块路径,之后生成依赖关系图需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  },
  getCode: ast => {
    // AST转换为code
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })
    return code
  }
}
class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模块
    this.modules = []
  }
  // 构建启动
  run() {
    // 解析入口文件
    const info = this.build(this.entry)
    this.modules.push(info)
    this.modules.forEach(({ dependecies }) => {
      // 判断有依赖对象,递归解析所有依赖项
      if (dependecies) {
        for (const dependency in dependecies) {
          this.modules.push(this.build(dependecies[dependency]))
        }
      }
    })
    // 生成依赖关系图
    const dependencyGraph = this.modules.reduce(
      (graph, item) => ({
        ...graph,
        // 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
        [item.filename]: {
          dependecies: item.dependecies,
          code: item.code
        }
      }),
      {}
    )
    this.generate(dependencyGraph)
  }
  build(filename) {
    const { getAst, getDependecies, getCode } = Parser
    const ast = getAst(filename)
    const dependecies = getDependecies(ast, filename)
    const code = getCode(ast)
    return {
      // 文件路径,可以作为每个模块的唯一标识符
      filename,
      // 依赖对象,保存着依赖模块路径
      dependecies,
      // 文件内容
      code
    }
  }
  // 重写 require函数,输出bundle
  generate(code) {
    // 输出文件路径
    const filePath = path.join(this.output.path, this.output.filename)
    const bundle = `(function(graph){
      function require(module){
        function localRequire(relativePath){
          return require(graph[module].dependecies[relativePath])
        }
        var exports = {};
        (function(require,exports,code){
          eval(code)
        })(localRequire,exports,graph[module].code);
        return exports;
      }
      require('${this.entry}')
    })(${JSON.stringify(code)})`
    // 把文件内容写入到文件系统
    fs.writeFileSync(filePath, bundle, 'utf-8')
  }
}
new Compiler(options).run()

webpack常用配置

SourceMap

webpack打包后如果文件出错会把错误指向打包后的文件中的某一行,而我们更需要知道是源文件哪一行出错,这时就需要配置source-map,在moudule.exports加入以下配置项

mode: 'development', // 表示是开发环境,js文件不压缩,设为 production 生产环境 则会压缩
devtool: 'cheep-module-eval-source-map' // 开发环境的最佳配置
//devtool: 'cheep-module-source-map', 生产环境的source-map的最佳配置

devtool配置项中

  • cheep表示只具体到某一行不具体到某一列,且不检测loader的错误,有助于加快编译速度;
  • module 检测loader的错误,因此错误更全,方便快速查找错误 ;
  • eval表示soucemap的映射代码放到打包后的js文件中,而不是生成source.map.js文件;
  • souce-map指将错误映射到具体源文件上
热加载

当希望更改源文件时能自动重新打包文件有两种方法,第一种是在package.json里配置scripts,缺点是还是得手动刷新页面,不够智能化

scripts: {
  watch: 'webpack --watch'
} 

一般推荐使用使用webpack-dev-server,npm install webpack-dev-server后,增加配置项

devServer: {
  contentBase: './dist', // 设置实时监听打包文件的目录
  open: true, // 自动打开浏览器
  port: 8080, // 端口
  hot: true, // 启动模块热更新
  hotOnly: true // 当模块热更新失败时浏览器也不自动刷新
  // proxy 可以配置跨域
}

当需要更改css文件时页面不刷新,则需要设置hot,启动HotModuleReplacement先引入webpack模块:const webpack = require(‘webpck’),再引入插件

plugins: [
  new webpack.HotModuleReplacementPlugin()
]

之后在package.json里配置启动脚本

 "scripts": {
  "start": 'webpack-dev-server'
} 

运行npm run start 即可热加载网页

编译ES6 兼容低版本IE

编译es6用的babel需要在根目录创建配置文件 .babelrc

{
  presets: [
    [
     "@babel/preset-env", {
       targets: {
         chrome: "67" // 谷歌浏览器自动编译es6语法,因此不用babel转换
       },
       useBuiltIns: "usage" // 按需引入map、Promise等低版本浏览器没有的对象
    }]
  ]
}

在IE低版本浏览器中是没有map、Promise等对象的,因此需要借用@babel/polyfill, npm install @babel/preset-env @babel/polyfill -D, 之后在js文件中import “@babel/polyfill”

识别打包图片、字体

npm install -D url-loader file-loader,两个loader均有将图片添加到dist目录里的功能

增加模块:

module: {
  rules: [
    {
      test: /\.(jpg|png|gif)$/,
      use: {
        loader: 'url-loader', // 功能跟file-loader差不多,区别是有转换base64的功能
        options: {
          name: '[name]_[hash].[ext]', // ext 是保留源文件后缀
          outputPath: 'images/', // dist 目录下的images文件夹
          limit: 10240 // 10kb以下的图片自动转换为base64编码插入到html中,其他正常生成图片
        }
      }
    }, {
      test: /\.(eot/ttf/svg)$/,
      use: {
        loader: 'file-loader'
      }
    }
  ]
}
打包前自动清除dist目录

打包前最好能自动清除dist 目录,防止冗余文件,npm i -D clean-webpack-plugin,引入插件const CleanWebpackPlugin = require(‘clean-webpack-plugn’),添加插件配置

plugins: [
  new CleanWebpackPlugin(['dist'])
]
常用loader

raw-loader:加载文件原始内容(utf-8)

file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)

url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)

source-map-loader:加载额外的 Source Map 文件,以方便断点调试

svg-inline-loader:将压缩后的 SVG 内容注入代码中

image-loader:加载并且压缩图片文件

json-loader加载 JSON 文件(默认包含)

handlebars-loader: 将 Handlebars 模版编译成函数并返回

babel-loader:把 ES6 转换成 ES5

ts-loader: 将 TypeScript 转换成 JavaScript

awesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader

sass-loader:将SCSS/SASS代码转换成CSS

css-loader:加载 CSS,支持模块化、压缩、文件导入等特性

style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS

postcss-loader:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀

eslint-loader:通过 ESLint 检查 JavaScript 代码

tslint-loader:通过 TSLint检查 TypeScript 代码

mocha-loader:加载 Mocha 测试用例的代码

coverjs-loader:计算测试的覆盖率

vue-loader:加载 Vue.js 单文件组件

i18n-loader: 国际化

cache-loader: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里

常用plugin

define-plugin:定义环境变量 (Webpack4 之后指定 mode 会自动配置)

ignore-plugin:忽略部分文件

html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)

web-webpack-plugin:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用

uglifyjs-webpack-plugin:不支持 ES6 压缩 (Webpack4 以前)

terser-webpack-plugin: 支持压缩 ES6 (Webpack4)

webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度

mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)

serviceworker-webpack-plugin:为网页应用增加离线缓存功能

clean-webpack-plugin: 目录清理

ModuleConcatenationPlugin: 开启 Scope Hoisting

speed-measure-webpack-plugin: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时)

webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)

webpack新版本特性以及版本功能对比

  • 剔除npm包里面针对Node.js模块自动引用的Polyfills

v4编译引入npm包,有些npm包里面包含针对nodejs的polyfills,实际前端浏览器是不需要的

例如:

// index.js
import CryptoJS from 'crypto-js';
const md5Password = CryptoJS.MD5('123123');
console.log(md5Password);

v4 引入crypto-js模块会自动引入polyfill: crypto-browserify, 但部分代码是不需要的,v5 默认会自动剔除
v5编译中,会出现polyfill添加提示,如果不需要node polyfille,按照提示 alias 设置为 false 即可

// webpack.config.js
  resolve: {
    // 1.不需要node polyfilss
    alias: {
      crypto: false
    },
    // 2.手动添加polyfills
    // fallback: {
    //   "crypto": require.resolve('crypto-browserify')
    // }
  }
  • 长期缓存优化

以前v4是根据代码的结构生成chunkhash,现在v5根据完全内容生成chunkhash,比如改了内容的注释或者变量则不会引起chunkhash的变化,让浏览器继续使用缓存

1:moduleId改为根据上下文模块路径计算,chunkId根据chunk内容计算

2: 为module,chunk 分配确定的(3或5位)数字ID,这是包大小和长期缓存之间的一种权衡

  • 持久化缓存

1:第一次构建是一次全量构建,它会利用磁盘模块缓存(以空间换时间),使得后续的构建从中获利。

2:后续构建具体流程是:读取磁盘缓存 -> 校验模块 -> 解封模块内容。

v5 默认情况,缓存配置是memory,修改设置为filesystem, 将缓存写入硬盘

// webpack.config.js
module.exports = { 
  cache: {
    // 1. 将缓存类型设置为文件系统
    type: 'filesystem', // 默认是memory
    // 2. 将缓存文件夹命名为 .temp_cache,
    // 默认路径是 node_modules/.cache/webpack
    cacheDirectory: path.resolve(__dirname, '.temp_cache')
  }
}
  • 模板联邦

跨项目间的chunk可以相互共享

1:UMD 模块

<script  src="https://unkpg.com/lodash.js"></script>

2: 微前端:多个项目共存于一个页面,有点类似iframe,共享的对象是项目级的,页面级的
子应用间的chunk以及对象可通过全局事件共享,但是公共包在项目安置以及打包编译很难放

子应用独立打包,模块解耦了,但公共的依赖不易维护处理
整体应用一起打包,能解决公共依赖;但庞大的多个项目又使打包变慢,后续也不好扩展

3:v5 的模块共享
这个方案是直接将一个应用的 bundle,应用于另一个应用,动态分发 runtime 子模块给其他应用。

模块联邦的使用方式如下:

module.exports = {
// other webpack configs...
plugins: [
new ModuleFederationPlugin({
// 1. name 当前应用名称,需要全局唯一
name: "app_one_remote",
// 2. remotes 可以将其他项目的 name 映射到当前项目中
remotes: {
app_two: "app_two_remote",
app_three: "app_three_remote"
},
// 3. exposes 表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用
exposes: {
AppContainer: "./src/App"
},
// 4. shared可以让远程加载的模块对应依赖改为使用本地项目的 React或ReactDOM。
shared: ["react", "react-dom", "react-router-dom"]
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
chunks: ["main"]
})
]
};

比如设置了remotes: { app_two: “app_two_remote” },在代码中就可以直接利用以下方式直接从对方应用调用模块

import { Search } from "app_two/Search";
app_two/Search来自于app_two 的配置:
// app_two的webpack 配置
export default {
plugins: [
new ModuleFederationPlugin({
name: "app_two",
library: { type: "var", name: "app_two" },
filename: "remoteEntry.js",
exposes: {
Search: "./src/Search"
},
shared: ["react", "react-dom"]
})
]
};

正是因为 Search在exposes被导出,我们因此可以使用 [name]/[exposes_name] 这个模块,这个模块对于被引用应用来说是一个本地模块。
构建优化

v4 有些场景是不能将无用代码剔除的

1: 对于模块引入嵌套场景,如下b 是不会出现在生产代码里面的

// one.js
export const a = 1;
export const b = 2;
// two.js
import * as inner from "./inner";
export { inner }
// three.js
import * as module from "./module";
console.log(module.inner.a);

2:只有 test 方法使用了 someting 。最终可以实现标记更多没有使用的导出项

import { something } from "./something";
function usingSomething() {
return something;
}
export function test() {
return usingSomething();
}

3: Commondjs。现在Webpack不仅仅支持 ES module 的 tree Shaking,commonjs规范的模块开始支持了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值