由浅及深,从零配置 webpack

在这里插入图片描述

本文由浅入深,讲述 webpack 如何从零开始配置项目打包所需。

建议配合视频食用 百度网盘 提取码:pc4r

欢迎各位一键三连,就当给我家淘淘攒狗粮了(#.#)

在这里插入图片描述

安装webpack

  • 全局安装
npm install webpack webpack-cli -g
  • 卸载命令
npm uninstall webpack webpack-cli -g
当然,并不推荐全局安装,所以可以在项目里面安装 webpack
  • 安装最新版本
npm install webpack webpack-cli -D
  • 安装指定版本
npm install webpack@4.25.0 webpack-cli -D
要是不知道 webpack 有什么版本号 ,输入
npm info webpack
就会打印出 webpack 所有的版本号
此时要是在终端输入
webpack -v
会出现报错
在安装的项目里面可以运行
npx webpack -v
就会打印出现在的版本号,原因是 npx 命令会去当前的项目目录里面寻找 node_modules去寻找 webpack

webpack 基础知识点

1. Webpack 是什么

Webpack 是一个前端资源加载/打包工具。它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。(大白话来说,就是模块打包工具,将多个模块打包到生成一个最终的bundle.js问题)

项目中想要使用 webpack 首先要 npm init -y 初始化,安装 webpack npm install webpack webpack-cli

根目录下新建一个 webpack.config.js

webpack 假定项目的入口起点为 src/index,会在 dist/bundle.js 输出结果,并且在生产环境开启压缩和优化。

const path = require('path')

module.exports = {
    // 入口文件
    entry: './src/index.js',
    // 出口文件
    output: {
        // 打包之后的文件名
        filename: 'bundle.js',
        // 打包之后文件的存放路径
        path: path.resolve(__dirname, 'dist')
    }
}

这是基本配置。

运行npx webpack index.js , 如果希望使用 npm run **** 来运行项目,则需要在 package.json里面配置

{
  "name": "day01",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
  	// 配置这个
    "bundle": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "webpack": "^4.42.1",
    "webpack-cli": "^3.3.11"
  }
}

打包出来的,终端输出的信息

Hash: 1097c683fef4192a87d3  // 哈希值
Version: webpack 4.42.1 // webpack 版本号
Time: 177ms // 花费时间
Built at: 2020-04-15 15:35:05 // 运行时间
    Asset      Size  Chunks             Chunk Names
bundle.js  1.36 KiB       0  [emitted]  main  //打包后的文件名,大小,id,入口文件名
Entrypoint main = bundle.js
[0] ./src/index.js 149 bytes {0} [built]
[1] ./src/header.js 208 bytes {0} [built]
[2] ./src/silder.js 208 bytes {0} [built]
[3] ./src/content.js 214 bytes {0} [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

底下警告的解决方式,在 webpack.config.js 里面,开头添加一句

module.exports = {
    mode: "production",
    // 入口文件
    entry: './src/index.js',
    // 出口文件
    output: {
        // 打包之后的文件名
        filename: 'bundle.js',
        // 打包之后文件的存放路径
        path: path.resolve(__dirname, 'dist')
    }
}

也可以添加一句 mode: "development" ,两者的不同在于:production 会对打包后的文件压缩。

production

在这里插入图片描述

development
在这里插入图片描述

webpack 核心概念

2. webpack loader

webpack可以使用 loader 来预处理文件,通过使用不同的Loader,webpack可以把不同的静态文件进行编译loader就是一个打包的方案,它知道对于某个特定的文件该如何去打包。 本身webpack不清楚对于一些文件如何处理,loader知道怎么处理,所以webpack就会去求助于loader。

常见的loader

  • raw-loader:加载文件原始内容(utf-8)
  • file-loader:将文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)
  • url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值时返回其 publicPath,小于阈值时返回文件 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
  • style-loader:将 CSS 代码注入 JavaScript 中,通过 DOM 操作去加载 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 之前添加,目的是将结果缓存到磁盘里
  • file-loader 处理图片打包 npm install file-loader -D
entry: './src/index.js',
  module: {
  rules: [{
    test: /\.(jpg|png|gif)$/,
    use: {
      loader: 'file-loader',
      options: {
        // placeholder 占位符
        // 打包后的图片名字,后缀和打包的之前的图片一样
        name: '[name]_[hash].[ext]',
        // 打包输出之后的文件夹
        outputPath: 'images/'
      }
    }
  }]
}
  • url-loader 也可以处理图片 npm install url-loader --save-dev
// 其他不变
loader: 'url-loader',

运行 npm run bundle之后,发现 dist 目录下并没有打包之后的图片,这时的图片是被打包在 bundle.js 里面,以 base64格式存在。

这样做的好处是减少请求的次数,缺点是如果图片太大,那么请求的时间也会过长。所以可以在添加 limit: 2048配置项。小于限制大小时,以base64格式存在 js 文件中 ,大于这个值时,以图片的形式存在

{
    test: /\.css$/,
    use: ['style-loader', 'css-loader'],
}

css-loader:分析页面css之间的关系,将所有的css文件都集合到一起,
style-loader:把 css-loader 处理好之后的文件,挂载到 header 之间

  • sass-loader 处理 sass 文件 npm install sass-loader sass --save-dev
{
  test: /\.scss$/,
  use: [
    'style-loader',
    'style-loader',
    'sass-loader'
  ],
}

loader 的执行顺序是 从下到上 从右到左
先加载sass-loader翻译成css文件,然后使用css-loader打包成一个css文件,通过style-loader挂载到页面上去。

  • postcss-loader 为了浏览器的兼容性,有时候我们必须加入-webkit,-ms,-o,-moz这些厂商前缀 npm i -D postcss-loader npm install autoprefixer -D

根目录下新建 postcss.config.js

module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}
{
  test: /\.scss$/,
  use: [
    'style-loader',
    'css-loader',
    'sass-loader',
    'postcss-loader'
  ],
}

使用 postcss-loader 的时候,会去寻找 postcss.config.js 文件,并且引用 autoprefixer 这个插件。

当在项目的 scss 文件当中也引用了 sass 文件,这时就需要添加 importLoaders: 2。这样无论你是在js中引入scss文件,还是在scss中引入scss文件,都会重新依次从下往上执行所以loader。

use: [
  'style-loader',
  {
    loader: 'css-loader',
    options: {
      // /sass文件里引入另外一个sass文件,另一个文件还会从postcss-loader向上解析
      // 如果不加,就直接从css-loader开始解析。
      importLoaders: 2,
      // 开启css的模块打包, css样式不会和其他模块发生耦合和冲突
      modules: true
    }
  },
  'sass-loader',
  'postcss-loader'
]
  • file-loader 处理字体图标 npm install file-loader --save-dev
{
  test: /\.(eot|ttf|svg)$/,
    use: {
    loader: 'file-loader'
  }
}

3. Webapck plugins

可以在webpack运行到某个时刻的时候,帮你做一些事情

常见的 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 输出文件的体积 (业务组件、依赖第三方模块)
  • htmlWebpackPlugin npm install --save-dev html-webpack-plugin htmlWebpackPlugin 会在打包结束后,自动生成一个html文件,并把打包生成的js自动引入到这个html文件中
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: "development",
  entry: './src/index.js',
  // .......
  plugins: [new HtmlWebpackPlugin({
    // 打包的时候,以 index.html 为模板。
    template: 'src/index.html'
  })],
  output: {
    // 打包之后的文件名
    filename: 'bundle.js',
    // 打包之后文件的存放路径
    path: path.resolve(__dirname, 'dist')
  }
}
  • CleanWebpackPlugin npm i clean-webpack-plugin -D 多次打包的时候,我们希望dist目录可以在每次打包的时候,自动删除,不会影响到下一次的打包
// ....
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  mode: "development",
  entry: './src/index.js',
  // .......
  plugins: [
    // 以 index.html 为模板。
    // 打包后执行
    new HtmlWebpackPlugin({ template: 'src/index.html' }),
    // 打包前执行
    new CleanWebpackPlugin()
  ],
  // 出口文件
  output: {
    // 打包之后的文件名
    filename: 'index.js',
    // 打包之后文件的存放路径
    path: path.resolve(__dirname, 'dist')
  }
}
Loader 和 Plugin 的区别
  • Loader 本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。
  • Plugin 就是插件,基于事件流框架 Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
  • Loader 在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。
  • Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。

4. Entry Ouput 的基本配置

  • 多个入口文件打包
entry: {
  main: './src/index.js',
  sub: './src/index.js'
},
output: {
  // 打包之后的文件名
  // 占位符:生成两个文件,不会报错,对于的名字就是entry名称对应
  filename: '[name].js',
  // 打包之后文件的存放路径
  path: path.resolve(__dirname, 'dist')
}
  • 将注入到 html 中的 js 文件前面加上地址
output: {
   // ...
   // 如果后台已经将资源挂载到了cdn上,那么你的publicPath就会把路径前做修改加上publicPath值
   publicPath: 'http://cdn.com.cn',
   // ...
}

5. source-map

source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre mapmap文件 只要不打开开发者工具,浏览器是不会加载的。

线上环境有三种处理方案:

hidden-source-map:借助第三方错误监控平台 Sentry 使用

nosources-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高

sourcemap:通过 nginx 设置将 .map 文件只对白名单开放(公司内网)

注意:避免在生产中使用 inline- 和 eval-,因为它们会增加 bundle 体积大小,并降低整体性能。

module.exports = {
  mode: "development",
  // 开启 source-map
  devtool: 'source-map',
  entry: {
    main: './src/index.js',
  },
  // .....
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  }
}
  • 开启 source-mapdevtool: source-map

  • map 文件会以 base64 格式存在于 main.js 文件中,映射到某一行的某一个字符(耗费性能):devtool: inline-source-map

  • 映射到某一行(不会映射到第三方模块错误): devtool: cheap-inline-source-map

  • 映射到某一行(映射到第三方模块错误): devtool: cheap-module-inline-source-map

  • 速度最快,性能最佳 --------- devtool: eval

  • 开发环境------------------- devtool: cheap-module-eval-source-map

  • 线上环境 ------------------- devtool: cheap-module-source-map

source-map 基本原理

在编译处理的过程中,在生成代码的同时,生成一个代码中被转换的部分与源代码中相应部分的映射关系表。有了这样一张完整的映射表,可以通过 Chrome 控制台中的"Enable Javascript source map"来实现调试时的显示与定位源代码功能。

6. WebpackDevServer 提升开发效率

package.json

"scripts": {
  // 运行 npm run watch 是 会监视要打包的文件,一旦发生变化,就会重新打包
  "watch": "webpack --watch",
    // 使用 webpack-dev-server 启动一个服务器
  "start": "webpack-dev-server"
},
module.exports = {
  mode: "development",
  devtool: 'cheap-module-eval-source-map',
  devServer: {
    // 配置开发服务运行时的文件根目录
    contentBase: './dist',
    // 自动打开浏览器
    // host:开发服务器监听的主机地址
    // compress :开发服务器是否启动gzip等压缩
    // port:开发服务器监听的端口
    open: true,
    // 接口代理
    proxy: {
      '/api': 'http://localhost:3000'
    }
  },
  // .......
  // 入口文件
  entry: {
    main: './src/index.js',
  },

  output: {
    // 打包之后的文件名
    filename: '[name].js',
    // 打包之后文件的存放路径
    path: path.resolve(__dirname, 'dist')
  }
}

devServer可以实时检测文件是否发生变化

使用 webpack-dev-server 打包的话,不会生成 dist 目录,而是将你的文件打包到内存中

7. Hot Module Replacement (HMR)热模块更新

const webpack = require('webpack')

module.exports = {
  mode: "development",
  devtool: 'cheap-module-eval-source-map',
  devServer: {
    contentBase: './dist',
    open: true,
    // 开启 HMR
    hot: true,
    // 即使 HMR 不生效,浏览器也不会刷新
    hotOnly: true
  },
  // ......
  plugins: [
    new HtmlWebpackPlugin({ template: 'src/index.html' }),
    new CleanWebpackPlugin(),
    // HMR
    new webpack.HotModuleReplacementPlugin()
  ]
}

注意的内容是,对于css的内容修改,css-loader底层会帮我们做好实时热更新,对于JS模块的话,我们需要手动的去配置。

热更新原理

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。

后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。

8. 使用 Babel 处理 ES6 语法

npm install --save-dev babel-loader @babel/core 是babel中的一个核心库

npm install @babel/preset-env --save-dev babel-loader并没有把es6 的语法转成 es5,只是两者之间搭建了一个桥梁而已,@babel/preset-env包含 es6转化成 es5的语法

npm install --save @babel/polyfill 补齐es6转 es5 缺少的函数。将Promise,map等低版本中没有实现的语法,用polyfill来实现。

{
  test: /\.js$/,
  // exclude参数: node_modules目录下的js文件不需要做转es5语法,也就是排除一些目录
  exclude: /node_modules/,
  loader: 'babel-loader',
  options: {
    // 大于 67版本的,不需要转化
    presets: [['@babel/preset-env', {
      "targets": {
        "chrome": "67",
      },
      // 有了preset-env这个模块后, const语法被翻译成成var。
      // 但对于Promise以及map这些语法,低版本浏览器是不支持的。
      // 需要@babel/polyfill模块,对Promise,map进行补充,完成该功能。
      // 但用完这个以后,打包的文件体积瞬间增加了10多倍之多。
	  // 因为@babel/polyfill为了弥补Promise,map等语法的功能,
      // 该模块就需要自己去实现Promise,map等语法的功能,这也就是为什么打包后的文件很大的原因。
      // 添加业务代码缺失的转化之后的函数,不会把所有的都添加进去
      useBuiltIns: 'usage'
    }]]
  }
},

开发库文件时,上面的场景使用babel会污染环境。这个时候,我们需要换一种方案来解决。

npm install --save-dev @babel/plugin-transform-runtime

npm install --save @babel/runtime

npm install --save @babel/runtime-corejs2(core 版本为 2 时,需要的配置)

{
  test: /\.js$/,
  exclude: /node_modules/,
  loader: 'babel-loader',
  options: {
    // 添加业务代码缺失的转化之后的函数
    // 大于 67版本的,不需要转化
    // presets: [['@babel/preset-env', {
    //     "targets": {
    //         "chrome": "67",
    //     },
    //     useBuiltIns: 'usage'
    // }]]
    "plugins": [[
      "@babel/plugin-transform-runtime", {
        "absoluteRuntime": false,
        "corejs": 2,
        "helpers": true,
        "regenerator": true,
        "useESModules": false,
        "version": "7.0.0-beta.0"
      }
    ]]
  }
},
Babel原理

大多数JavaScript Parser遵循 estree 规范,Babel 最初基于 acorn 项目(轻量级现代 JavaScript 解析器)

Babel大概分为三大部分:

  • 解析:将代码转换成 AST(抽象语法树)
  • 词法分析:将代码(字符串)分割为 token 流,即语法单元成的数组
  • 语法分析:分析 token 流(上面生成的数组)并生成 AST
  • 转换:访问 AST 的节点进行变换操作生产新的 AST
  • 生成:以新的 AST 为基础生成代码

9. TreeShaking

如果程序是一棵树。绿色表示实际用到的源码和 library,是树上活的树叶。灰色表示无用的代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。

通俗意义而言,当你引入一个模块时,你可能用到的只是其中的某些功能,这个时候,我们不希望这些无用的代码打包到项目中去。通过tree-shaking,就能将没有使用的模块摇掉,这样达到了删除无用代码的目的。

webpack4默认的production下是会进行tree-shaking的

mode: "development",
// ......
plugins: [
  new HtmlWebpackPlugin({template: 'src/index.html'
  new CleanWebpackPlugin(),
  new webpack.HotModuleReplacementPlugin()
],
//在开发环境中加,生产环境不加
optimization: {
  usedExports: true
},
// 出口文件
output: {
  filename: '[name].js',
  path: path.resolve(__dirname, 'dist')
}
side-effect-free

如果我们的模块不是达到很纯粹,这个时候,webpack就无法识别出哪些代码需要删除,所以,此时有必要向 webpack 的 compiler 提供提示哪些代码是“纯粹部分”。

这种方式是通过 package.json 的 "sideEffects" 属性来实现的。

{
  "name": "webpack-demo",
  "sideEffects": false
}

如同上面提到的,如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack,它可以安全地删除未用到的 export 导出。

注意,任何导入的文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader 并导入 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:

{
  "name": "webpack-demo",
  "sideEffects": [
    "*.css"
  ]
}

10. Develoment 和 Production 模式的区分打包

npm install webpack-merge -D

开发环境和生成环境中,所侧重的功能是不一样的

  • 开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。

  • 生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。

把 webpack.config.js 文件修改为 webpack.dev.js,新建 webpack.prod.js 复制一份 webpack.dev.js 中的内容
,新建一个 webpack.common.js 文件(作为公共模块)

webpack.common.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  entry: {
    main: './src/index.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader'
      },
      {
        test: /\.(jpg|png|gif)$/,
        use: {
          loader: 'url-loader',
          options: {
            name: '[name]_[hash].[ext]',
            outputPath: 'images/',
            limit: 2048
          }
        }
      }, {
        test: /\.scss$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 2
            }
          },
          'sass-loader',
          'postcss-loader'
        ],
      }, {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader'
        ]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({ template: 'src/index.html' }),
    new CleanWebpackPlugin()
  ],
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  }
}

webpack.dev.js

const webpack = require('webpack')
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')

const devConfig = {
  mode: "development",
  devtool: 'cheap-module-eval-source-map',
  devServer: {
    contentBase: './dist',
    open: true,
    hot: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],
  optimization: {
    usedExports: true
  },
}
// 合并两个模块
module.exports = merge(commonConfig, devConfig)

webpack.prod.js

const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')

const prodConfig = {
  mode: "production",
  devtool: 'cheap-module-source-map'
}
// 合并两个模块
module.exports = merge(commonConfig, prodConfig)

一般的脚手架中,会有一个 build 文件夹,我们就可以把刚刚的三个文件移入其中。

package.json

"scripts": {
  "dev-build": "webpack --config ./build/webpack.dev.js",
  "dev": "webpack-dev-server --config ./build/webpack.dev.js",
  "build": "webpack --config ./build/webpack.prod.js"
},

11. Code Splitting 代码分割

代码分割的本质其实就是在源代码直接上线和打包成唯一脚本main.bundle.js这两种极端方案之间的一种更适合实际场景的中间状态。有什么意义呢? 用可接受的服务器性能压力增加来换取更好的用户体验

代码分割,和webpack无关,为了提升性能,webpack有插件,可以很好的实现代码分割

如何实现代码分割
  • 同步代码: 只需要在webpack.common.js中做optimization的配置即可
plugins: [
  new HtmlWebpackPlugin({ template: 'src/index.html' }),
  new CleanWebpackPlugin()
],
// 代码分割
optimization: {
  splitChunks: {
    chunks: 'all'
  }
},
// 代码分割
output: {
  filename: '[name].js',
  path: path.resolve(__dirname, '../dist')
}
  • 异步代码(import): 异步代码,无需做任何配置,会自动进行代码分割,放置到新的文件中

12. SplitChunksPlugin 配置参数详解

当有多个入口文件,或者是打包文件需要做一个划分,举个例子,比如第三方库lodash,jquery等库需要打包到一个目录下,自己的业务逻辑代码需要打包到一个文件下,这个时候,就需要提取公共模块了。

npm install --save-dev @babel/plugin-syntax-dynamic-import

.babelrc

{
  "presets": [
    [
      "@babel/preset-env", {
      "targets": {
        "chrome": "67"
      },
      "useBuiltIns": "usage"
    }
  ],
  "@babel/preset-react"
  ],
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}

webpack.common.js

optimization: {
  splitChunks: { //启动代码分割,不写有默认配置项
    chunks: 'all', //参数all/initial/async,只对所有/同步/异步进行代码分割
    minSize: 30000, //大于30kb才会对代码分割
    maxSize: 0,
    minChunks: 1,//打包生成的文件,当一个模块至少用多少次时才会进行代码分割
    maxAsyncRequests: 5,//同时加载的模块数最多是5个
    maxInitialRequests: 3,//入口文件最多3个模块会做代码分割,否则不会
    automaticNameDelimiter: '~',//文件自动生成的连接符
    name: true,
    cacheGroups: {
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        priority: -10, //谁优先级大就把打包后的文件放到哪个组
        filename: 'vendors.js'
      },
      default: {
        minChunks: 2,
        priority: -20,
        reuseExistingChunk: true,//模块已经被打包过了,就不用再打包了,复用之前的就可以
        filename: 'common.js' //打包之后的文件名   
      }
    }
  }
},

14. Lazy Loading 懒加载,Chunk是什么?

用户当前需要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载 在给单页应用做按需加载优化时,一般采用以下原则:

  • 对网站功能进行划分,每一类一个chunk
  • 对于首次打开页面需要的功能直接加载,尽快展示给用户
  • 某些依赖大量代码的功能点可以按需加载
  • 被分割出去的代码需要一个按需加载的时机
    每一个文件就是一个 chunk
    (chunk的中文翻译是 大块的意思)
    一般我们采用默认配置即可,不需要修改
plugins: [
  new HtmlWebpackPlugin({ template: 'src/index.html' }),
  new CleanWebpackPlugin()
],
optimization: {
  splitChunks: {
    chunks: 'all',
  }
},
output: {
  filename: '[name].js',
  path: path.resolve(__dirname, '../dist')
}

15. CSS 代码分割

output: {
  filename: '[name].js',
  // main.js异步加载的间接的js文件。用来打包import('module')方法中引入的模块
  // 第三方模块
  chunkFilename: '[name].chunk.js',
  path: path.resolve(__dirname, '../dist')
}

把CSS单独提取出来加载, 提高性能
npm install --save-dev mini-css-extract-plugin:一般在线上环境使用这个插件,因为在开发环境中不支持HMR。

npm i optimize-css-assets-webpack-plugin -D压缩css文件
改造我们的三个webpack 配置文件

webpack.common.js

module.exports = {
  // .....
  output: {
    filename: '[name].js',
    // main.js异步加载的间接的js文件。用来打包import('module')方法中引入的模块
    // 第三方模块
    chunkFilename: '[name].chunk.js',
    path: path.resolve(__dirname, '../dist')
  }
}

webpack.prod.js

// 引入两个插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

const prodConfig = {
  mode: "production",
  devtool: 'cheap-module-source-map',
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 2
            }
          },
          'sass-loader',
          'postcss-loader'
        ]
      },
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader'
        ]
      }
    ]
  },
  optimization: {
    minimizer: [new OptimizeCSSAssetsPlugin({})],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css', //直接引用的css文件
      chunkFilename: '[name].chunk.css' //间接引用的css文件
    })
  ]
}

module.exports = merge(commonConfig, prodConfig)

package.json

"sideEffects": [
    "*.css" //除了css文件,其余的都TreeShaking
  ],

16. Webpack 与浏览器缓存(Caching)

当你项目即将上线时,有一个需求,你只是修改了部分的文件,只希望用户对于其他的文件,依旧去采用浏览器缓存中的文件,所以这个时候,我们需要用到contenthash

webpack中关于hash,有三种,分别是

**hash:**主要用于开发环境中,在构建的过程中,当你的项目有一个文件发现了改变,整个项目的hash值就会做修改(整个项目的hash值是一样的),这样子,每次更新,文件都不会让浏览器缓存文件,保证了文件的更新率,提高开发效率。

**chunkhash:**跟打包的chunk有关,具体来说webpack是根据入口entry配置文件来分析其依赖项并由此来构建该entry的chunk,并生成对应的hash值。不同的chunk会有不同的hash值。

在生产环境中,我们会把第三方或者公用类库进行单独打包,所以不改动公共库的代码,该chunkhash就不会变,可以合理的使用浏览器缓存了。

但是这个中hash的方法其实是存在问题的,生产环境中我们会用webpack的插件,将css代码打单独提取出来打包。这时候chunkhash的方式就不够灵活,因为只要同一个chunk里面的js修改后,csschunkhash也会跟随着改动。因此我们需要contenthash

contenthash:contenthash表示由文件内容产生的hash值,内容不同产生的contenthash值也不一样。生产环境中,通常做法是把项目中css都抽离出对应的css文件来加以引用。

对于webpack,旧版本而言,即便每次你npm run build,内容不做修改的话,contenthash值还是会有所改变,这个是因为,当你在模块之间存在相互之间的引用关系,有一个manifest文件

manifest文件是用来引导所以模块的交互,manifest文件包含了加载和处理模块的逻辑,举个例子,你的第三方库打包后的文件,我们称之为vendors,你的逻辑代码称为main,当你webpack生成一个bundle时,它同时会去维护一个manifest文件,你可以理解成每个bundle文件存在这里信息,所以每个bundle之间的manifest信息有不同,这样子我们就需要将manifest文件给提取出来。

这个时候,需要在optimization中增加一个配置

webpack.common.js

optimization: {
  //兼容老版本webpack4,把manifest打包到runtime里,不影响业务代码和第三方模块
  runtimeChunk: {
    name: 'manifest'
  },
  usedExports: true,
  splitChunks: {
    // ...
  }
},
performance: false, //禁止提示性能上的一些问题
output: {
  path: path.resolve(__dirname, '../dist')
}

webpack.dev.js

plugins: [
  new webpack.HotModuleReplacementPlugin()
],
output: {
  filename: '[name].js',
  chunkFilename: '[name].js',
}

webpack.prod.js

optimization: {
  minimizer: [new OptimizeCSSAssetsPlugin({})],
},
plugins: [
  new MiniCssExtractPlugin({
    filename: '[name].css',
    chunkFilename: '[name].chunk.css'
  })
],
output: {
  filename: '[name].[contenthash].js', 
  // 源代码不变,hash值就不会变,解决浏览器缓存问题。
  // 打包上线时,用户只需要更新有变化的代码,没有变化的从浏览器缓存读取
  chunkFilename: '[name].[contenthash].js'
}

17. Shimming (垫片)

当你再使用第三方库,此时需要引入它,又或者是你有很多的第三方库或者是自己写的库,每个js文件都需要依赖它,让人很繁琐,这个时候,shimming就派上用场了。

const webpack = require('webpack')
// ....

plugins: [
  new HtmlWebpackPlugin({ template: 'src/index.html' }),
  new CleanWebpackPlugin(),
  // 发现模块使用 $ ,会自动引用 jquery
  new webpack.ProvidePlugin({
    $: 'jquery',
    _join: ['lodash', 'join']//_join代表lodash里的join方法
  })
],

项目中的文件

export const Arr_add = arr=>{
  let str = _.join(arr,'++');
  return str;
}

18. 环境变量的使用方法

只需要一个common.js文件通过在package.json中传递不同的参数,区分是开发环境还是生产环境。

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack')
const merge = require('webpack-merge')
const devConfig = require('./webpack.dev')
const prodConfig = require('./webpack.prod')

const commonConfig = {
  entry: {
    main: './src/index.js',
  },
  module: {
    // ....
  },
  plugins: [
    // .....
  ],
  optimization: {
    // ....
  },
  performance: false, //禁止提示性能上的一些问题
  output: {
    path: path.resolve(__dirname, '../dist')
  }
}

module.exports = (env) => {
  if (env && env.production) {
    return merge(commonConfig, prodConfig)
  } else {
    return merge(commonConfig, devConfig)
  }
}

通过–env.production,把环境变量传进去

"scripts": {
  "dev-build": "webpack --config ./build/webpack.common.js",
  "dev": "webpack-dev-server --config ./build/webpack.common.js",
  "build": "webpack --env.production --config ./build/webpack.common.js"
},

19. 库文件的打包

webpack.config.js

具体代码

const path = require('path');

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  // 忽略 lodash ,不打包
  // externals: ['lodash'],
  externals: {
    // 当用户引用 lodash 这个库文件的时候,必须使用 lodash 这个名字
    lodash: {
      commonjs: 'lodash'
    }
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'library.js',
    library: 'library',
    // 支持 script 标签引入
    libraryTarget: 'umd'
    // import const require 无论什么方法引入,都支持
    // libraryTarget: 'this'
    // library 挂载到 this
  }
}

package.json

"main": "./dist/library.js",
  "scripts": {
    "build": "webpack"
},

20. PWA的打包(Progressive Web Application)

在离线(offline)时应用程序能够继续运行功能。这是通过使用名为 Service Workers 的 web 技术来实现的。线上环境时才用到pwa,开发时不需要

为了模拟环境,安装npm i http-server -D npm install workbox-webpack-plugin --save-dev
具体代码

package.json

"scripts": {
  "start": "http-server dist",
  "dev": "webpack-dev-server --config ./build/webpack.common.js",
  "build": "webpack --env.production --config ./build/webpack.common.js"
},

webpack.prod.js

const WorkboxPlugin = require('workbox-webpack-plugin');
// ....
plugins: [
  new MiniCssExtractPlugin({
    filename: '[name].css',
    chunkFilename: '[name].chunk.css'
  }),
  new WorkboxPlugin.GenerateSW({
    clientsClaim: true,
    skipWaiting: true
  })
],

21. TypeScript 的打包配置

npm install --save-dev typescript ts-loader

npm install --save-dev @types/lodash

webpack.config.js

const path = require('path')

module.exports = {
  mode: 'production',
  entry: './src/index.tsx',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
}

tsconfig.json

{
  "compilerOptions": {
    "outDir": "./dist",
    "module": "es6",
    "target": "es5",
    "allowJs": true
  }
}

typescript

22. WebpackDevServer 实现请求转发

npm i axios -D

以 react 为例

index.js
class App extends Component {

  componentDidMount() {
    axios.get('/react/api/header.json').then((res) => {
      console.log(res)
    })
  }

  render() {
    return <div>hello react</div>
  }
}

webpack.config.js

devServer: {
  contentBase: './dist',
  open: true,
  hot: true,
  hotOnly: true,
  proxy: {
    '/react/api': {
      target: 'http://www.naiyoutaozi.com',
      //对https协议的网址的请求的转发
      secure: false,
      // bypass: function (req, res, proxyOptions) {
      //     // 请求是 html 的情况,直接返回这个 html ,不会走代理
      //     if (req.headers.accept.indexOf('html') !== -1) {
      //         console.log('Skipping proxy for browser request.');
      //         return '/index.html';
      //     }
      // },
      pathRewrite: {
          'header.json': 'demo.json'
      },
      //解决网站对接口的限制
      changeOrigin: true,
      //变更请求头
      headers: {
        host: 'www.dell-lee.com',
        cookie: 'asvafhgs'
      }
    }
  }
},

23. WebpackDevServer 解决单页面应用路由问题

npm i react-router-dom --save

proxy: {
  '/react/api': {
    target: 'http://www.dell-lee.com',
    secure: false,
    // historyApiFallback: {
    //     rewrites: [//访问任何路径都展示index.html页面
    //         { from: /\.*/, to: '/index.html' },
    //     ]
    // },
    // 一般配置这个就可以了
    historyApiFallback: true,
    // bypass: function (req, res, proxyOptions) {
    //     // 请求是 html 的情况,直接返回这个 html ,不会走代理
    //     if (req.headers.accept.indexOf('html') !== -1) {
    //         console.log('Skipping proxy for browser request.');
    //         return '/index.html';
    //     }
    // },
    pathRewrite: {
      'header.json': 'demo.json'
    },
    changeOrigin: true,
    headers: {
      host: 'www.dell-lee.com',
      cookie: 'asvafhgs'
    }
  }
}

24. EsLint 在 Webpack 中的配置

npm install eslint -D

npm install babel-eslint -D

npm install eslint-loader -D

//快速生成eslint配置
npx eslint --init

.eslintrc.js

module.exports = {
    "env": {
        "browser": true,
        "es6": true
    },
    "extends": [
        "plugin:react/recommended",
        "airbnb"
    ],
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    "parser": "babel-eslint",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    "plugins": [
        "react"
    ],
    "rules": {
    }
};

webpack.config.js

devServer: {
    // eslint 报错显示在浏览器
    overlay: true,
    contentBase: './dist',
},

{
    test: /\.js$/,
    exclude: /node_modules/,
    // use: [
    //     'babel-loader',
    //     {
    //         loader: 'eslint-loader',
    //         options: {
    //             fix: true
    //         }
    //     }
    // ]
    // use: [
    //     {
    //         loader: 'eslint-loader',
    //         options: {
    //             fix: true
    //         },
    //         // 强制执行
    //         force: 'pre'
    //     },
    //     'babel-loader'
    // ]
    loader: 'babel-loader'
},

25. Webpack 性能优化

  • 使用高版本的 Webpack 和 Node.js

  • 多进程/多实例构建:thread-loader, parallel-webpack,happypack

  • 压缩代码

    • webpack-paralle-uglify-plugin
    • uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
    • terser-webpack-plugin 开启 parallel 参数
    • 多进程并行压缩
    • 通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。
  • 图片压缩

    • 使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式)
    • 配置 image-webpack-loader
  • 缩小打包作用域

    • exclude/include (确定 loader 规则范围)
    • resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
    • resolve.mainFields 只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
    • resolve.extensions 尽可能减少后缀尝试的可能性
    • noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
    • IgnorePlugin (完全排除模块)
      • 合理使用alias
  • 提取页面公共资源

    • 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
    • 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
    • 基础包分离
  • DLL:

    • 使用 DllPlugin 进行分包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间。实现第三方模块只打包一次
    • HashedModuleIdsPlugin 可以解决模块数字id问题
  • 充分利用缓存提升二次构建速度:

    • babel-loader 开启缓存
    • terser-webpack-plugin 开启缓存
    • 使用 cache-loader 或者 hard-source-webpack-plugin
  • Tree shaking

    • purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)
    • 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率
    • 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
    • 使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码
  • Scope hoisting

    • 构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
    • 必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法
  • 动态Polyfill

    • 建议采用 polyfill-service 只给用户返回需要的polyfill,社区维护。(部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill)

26. 编写一个 Loader

Loader 支持链式调用,所以开发上需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事情。
  • Loader 运行在 Node.js 中,我们可以调用任意 Node.js 自带的 API 或者安装第三方模块进行调用
  • Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串,当某些场景下 Loader 处理二进制文件时,需要通过 exports.raw = true 告诉 Webpack 该 Loader 是否需要二进制数据
  • 尽可能的异步化 Loader,如果计算量很小,同步也可以
  • Loader 是无状态的,不应该在 Loader 中保留状态
  • 使用 loader-utils 和 schema-utils 提供的实用工具

27. 如何编写一个 Plugin

webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在特定的阶段钩入想要添加的自定义功能。Webpack 的 Tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值