从基础到实战 手把手带你掌握新版Webpack4.0(学习笔记)

01 webpack 初探-导学

传统编程的弊端

以前使用面向对象编程,页面需要引入多个js,造成多个请求,影响加载,需要注意引用的顺序,使用时无法直接从js里看出文件的层级关系,一旦出错调试很麻烦

// /index.html
<div id="root"></div>
<script src="./header.js"></script>
<script src="./index.js"></script>

// /header.js
function Header() {
  var root = document.getElementById('root')
  var header = document.createElement('div')
  header.innerText = 'header'
  root.appendChild(header)
}

// /index.js
new Header()

使用 webpack(模块打包工具) 编程

解决传统编程的弊端

mkdir webpack-test # 创建项目文件夹
rmdir webpack-test # 删除文件夹
cd webpack-test # 进入项目文件夹
npm init # 初始化包管理器
npm init -y # 不一步步的询问配置项,直接生成一个默认的配置项
npm install webpack-cli --save-dev # 安装 webpack-cli (作用是使我们可以在命令行里运行 webpack)
npm uninstall webpack-cli --save-dev # 卸载 webpack-cli
npm install webpack --save # 安装 webpack
npm info webpack # 查看 webpack 的相关信息
npm install webpack@4.25.0 -S # 安装指定版本号的 webpack

创建文件写代码:

// /index.html
<div id="root"></div>

// /header.js
function Header() {
  var root = document.getElementById('root')
  var header = document.createElement('div')
  header.innerText = 'header'
  root.appendChild(header)
}
export default Header

// /index.js
import Header from './header.js'
new Header()

npx webpack index.js # 编译 index.js 文件,生成 ./dist/main.js 文件

// /index.html 中引入编译后的文件
<script src="./dist/main.js"></script>

不同的模块引入方式

  • ES Module 模块引入方式
export default Header // 导出
import Header from './header.js' // 引入
  • CommonJS 模块引入方式
module.exports = Header // 导出
var Header = require('./header.js') // 引入

附录


02 webpack 初探-配置

webpack 的安装

  • 最好不要全局安装 webpack,防止不同项目使用不同的 webpack 版本,无法同时运行!
  • 将 webpack 直接安装在项目中,无法使用全局的 webpack 命令,可以在前面加上 npx,表示从当前目录下去找 webpack,例如:npx webpack -v
// package.json
{
  "private": true, // 表示该项目是私有项目,不会被发送到 npm 的线上仓库
  "main": "index.js", // 如果项目不被外部引用,则不需要向外部暴露一个 js 文件,可将该行删除
  "scripts": { // 配置 npm 命令, 简化命令行输入的命令
    "build": "webpack", // 不用加 npx, 会优先从项目目录中去找 webpack; 配置之后可使用 npm run build 代替 npx webpack
  }
}

webpack 的配置文件

  • webpack 默认配置文件是 webpack.config.js
  • npx webpack --config wConfig.js # 让 webpack 以 wConfig.js 文件为配置文件进行打包
// webpack.config.js
const path = require('path') // 引入一个 node 的核心模块 path

module.exports = {
  entry: './index.js', // 打包入口文件
  // entry: { // 上面是该种写法的简写
  //   main: './index.js'
  // },
  output: {
    filename: 'main.js', // 打包后的文件名
    path: path.resolve(__dirname, 'dist') // 打包后文件的路径(要指定一个绝对路径); 通过 path 的 resolve 方法将当前路径(__dirname)和指定的文件夹名(dist)做一个拼接
  },
  mode: 'production', // 配置打包的模式(production/development); 生产模式(会压缩)/开发模式(不会压缩)
}

webpack 打包输出信息

Hash: d8f9a3dacac977cc0968 # 打包对应的唯一 hash 值
Version: webpack 4.40.2 # 打包使用的 webpack 版本
Time: 208ms # 打包消耗的时间
Built at: 2019-09-20 16:38:59 # 打包的当前时间
  Asset       Size  Chunks             Chunk Names
# 生成的文件 文件大小 文件对应id 文件对应名字
main.js  930 bytes       0  [emitted]  main
Entrypoint main = main.js # 打包的入口文件
[0] ./index.js 36 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/

03 webpack 的核心概念-loader

webpack 默认知道如何打包 js 文件,loader 的作用就是告诉 webpack 如何打包其它不同类型的文件

打包图片类型的文件

file-loader

使用 file-loader 打包一些图片文件(需要执行命令 npm i file-loader -D 安装 file-loader)

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      // test: /\.jpg$/,
      test: /\.(jpg|png|gif)$/, // 配置允许匹配多个文件类型
      use: {
        loader: 'file-loader',
        options: {
          name: '[name]_[hash].[ext]', // 配置打包后文件的名称(name:文件原名;hash:哈希值;ext:文件后缀;最终生成:文件原名_哈希值.文件后缀),若不配置,文件会以哈希值命名
          outputPath: 'static/img/' // 配置打包后文件放置的路径位置
        }
      }
    }]
  }
}

url-loader

与 file-loader 类似,还可以使用 url-loader 打包一些图片文件(同样需要先安装)

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.(jpg|png|gif)$/,
      use: {
        loader: 'url-loader',
        options: {
          name: '[name]_[hash].[ext]',
          outputPath: 'static/img/',
          limit: 10240 // 与 file-loader 不同的是,可以配置 limit 参数(单位:字节),当文件大于 limit 值时,会生成独立的文件,小于 limit 值时,直接打包到 js 文件里
        }
      }
    }]
  }
}

注:url-loader 依赖 file-loader,使用 url-loader 同时需要安装 file-loader

打包样式文件

在 webpack 的配置里,loader 是有先后顺序的,loader 的执行顺序是从下到上,从右到左的

打包 css / sass

注:node-sass无法安装时,可采用cnpm或查看node-sass无法安装时的解决办法

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.css$/, // .css 结尾的文件使用 style-loader 和 css-loader 打包(需要安装 style-loader 和 css-loader)
      use: ['style-loader', 'css-loader'] // css-loader 会帮我们分析出多个 css 文件之间的关系,将多个 css 合并成一个 css;style-loader 将 css-loader 处理好的 css 挂载到页面的 head 部分
    }, {
      test: /\.scss$/, // .scss 结尾的文件使用 style-loader 和 css-loader 和 sass-loader 打包(需要安装 style-loader 和 css-loader 和 sass-loader 和 node-sass)
      use: ['style-loader', 'css-loader', 'sass-loader'] // 这里先执行 sass-loader 将 sass 代码翻译成 css 代码;然后再由 css-loader 处理;都处理好了再由 style-loader 将代码挂载到页面上
    }]
  }
}

// index.js
// 配置好之后在 js 中引入 css 即可
import './index.css'

打包时自动添加厂商前缀

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.css/,
      use: ['postcss-loader'] // 需要执行 npm i postcss-loader -D 安装 postcss-loader
    }]
  }
}

// postcss.config.js // 在根目录下创建该文件
module.exports = {
  plugins: [
    require('autoprefixer') // 需要执行 npm i -D autoprefixer 安装 autoprefixer
  ]
}

给 loader 增加配置项

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.css/,
      use: ['style-loader', {
        loader: 'css-loader',
        options: {
          importLoaders: 1 // 有时候会在一个样式文件里 import 另一个样式文件,这就需要配置 importLoaders 字段,是指在当前 loader 之后指定 n 个数量的 loader 来处理 import 进来的资源(这里是指在 css-loader 之后使用 sass-loader 来处理 import 进来的资源)
        }
      }, 'sass-loader']
    }]
  }
}

css 打包模块化(避免全局引入)

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.css$/,
      use: ['style-loader', {
        loader: 'css-loader',
        options: {
          modules: true // 开启 css 的模块化打包
        }
      }]
    }]
  }
}

// index.css(若使用 sass,增加对应 loader 即可)
.avatar {
  width: 100px;
  height: 100px;
}

// index.js
import style from './index.css'
var img = new Image()
img.src = ''
img.classList.add(style.avatar)

打包字体文件

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.(eot|ttf|svg)$/,
      use: ['file-loader']
    }]
  }
}

04 webpack 的核心概念-plugin

plugin 可以在 webpack 运行到某个时刻的时候帮你做一些事情(类似 vue 的生命周期函数一样)

html-webpack-plugin(v3.2.0)

  • 时刻:在打包之后开始运行
  • 作用:会在打包结束后,自动生成一个 html 文件,并将打包生成的 js 自动引入到这个 html 文件中
  • 安装:npm i html-webpack-plugin -D
  • 使用:
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  plugins: [new HtmlWebpackPlugin({
    template: 'index.html' // 指定生成 html 的模版文件(如果不指定,则会生成一个默认的不附带其它内容的 html 文件)
  })]
}

clean-webpack-plugin(v3.0.0)

  • clean-webpack-plugin 升级3.0踩坑
  • 时刻:在打包之前开始运行
  • 作用:删除文件夹目录(默认删除 output 下 path 指定的目录)
  • 安装:npm i clean-webpack-plugin -D
  • 使用:
// webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  plugins: [new CleanWebpackPlugin({
    cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, 'dist')] // 若不配置,默认删除 output 下 path 指定的目录
  })]
}

05 webpack 的核心概念-entry&output

打包单个文件

// webpack.config.js
const path = require('path')

module.exports = {
  // entry: './src/index.js', // 简写方式
  entry: {
    main: './src/index.js'
  },
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  }
}

打包多个文件

// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: {
    index1: './src/a.js',
    index2: './src/b.js'
  },
  output: {
    publicPath: 'http://cdn.com.cn', // 会在自动生成的 html 文件中,引入文件路径的前面加上此路径
    filename: '[name].[hash].js', // name 即指 entry 中配置的需要打包文件的 key (也即 index1 和 index2, 最终会生成 index1.js 和 index2.js)
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [new HtmlWebpackPlugin()]
}

06 webpack 的核心概念-sourceMap

// webpack.config.js
module.exports = {
  devtool: 'source-map'
  // devtool: 'cheap-module-eval-source-map' // 常用于开发环境
  // devtool: 'cheap-module-source-map' // 常用于生产环境
}
  • devtool 的可能值:
devtool 解释
none 不生成 sourceMap
source-map 生成 .map 文件
inline-source-map 不生成 .map 文件,sourceMap 会被合并到打包生成的文件里
cheap-source-map 只告诉出错的行,不告诉出错的列
cheap-module-source-map 除了业务代码里的错误,还要提示一些 loader 里面的错误
eval 不生成 .map 文件,使用 eval 在打包后文件里生成对应关系

07 webpack 的核心概念-WebpackDevServer

--watch

在 webpack 命令后面加 --watch,webpack 会监听打包的文件,只要文件发生变化,就会自动重新打包

// package.json
{
  "scripts": {
    "watch": "webpack --watch"
  }
}

webpack-dev-server

  • npm i webpack-dev-server -D # 安装 webpack-dev-server 包
  • 配置
// webpack.config.js
module.exports = {
  devServer: {}
}

// package.json
{
  "scripts": {
    "wdserver": "webpack-dev-server"
  }
}
  • npm run wdserver # 编译项目到内存中,并启动一个服务
  • devServer 有很多可配置的参数:
open: true // 启动服务的时候自动在浏览器中打开当前项目(默认 false)
port: 8888 // 自定义启动服务的端口号(默认 8080)

contentBase: './static' // 指定资源的请求路径(默认 当前路径)
例如:
/static 文件夹下存在一张图片 /static/img.png
devServer 里配置 contentBase: './static'
/index.html 中使用 <img src="img.png" />
这样它就会去 /static 文件夹下去找 img.png 而不是从根目录下去找 img.png

express & webpack-dev-middleware

借助 express 和 webpack-dev-middleware 自己手动搭建服务

  • npm i express webpack-dev-middleware -D # 安装 express 和 webpack-dev-middleware 包
  • 根目录下新建 server.js
// server.js(在 node 中使用 webpack)
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackConfig = require('./webpack.config.js')
const complier = webpack(webpackConfig)

const app = express()

app.use(webpackDevMiddleware(complier, {}))

app.listen(3000, () => {
  console.log('server is running at port 3000')
})
  • 配置 npm 命令
// package.json
{
  "scripts": {
    "nodeserver": "node server.js"
  }
}
  • npm run nodeserver # 编译项目并启动服务(成功后在浏览器输入 localhost:3000 访问项目)

附录

在命令行中使用 webpack

webpack index.js -o main.js # 编译 index.js 输出 main.js


08 webpack 的核心概念-HotModuleReplacementPlugin

HotModuleReplacementPlugin 是 webpack 自带的一个插件,不需要单独安装

配置

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

module.exports = {
  devServer: {
    hot: true, // 让 webpack-dev-server 开启 hot module replacement 这样的一个功能
    hotOnly: true // 即便是 hot module replacement 的功能没有生效,也不让浏览器自动刷新
  },
  plugins: [new webpack.HotModuleReplacementPlugin()]
}

在 css 中使用

更改样式文件,页面就不会整个重新加载,而是只更新样式

// /index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>html 模板</title>
  </head>
  <body></body>
</html>
// /src/index.css
div {
  width: 100px;
  height: 100px;
}
div:nth-of-type(odd) {
  background-color: rgb(255, 0, 0);
}
// /src/index.js
import './index.css'

var btn = document.createElement('button')
btn.innerText = 'button'
document.body.appendChild(btn)

btn.onclick = () => {
  var item = document.createElement('div')
  item.innerText = 'item'
  document.body.appendChild(item)
}
// /package.json
{
  "name": "webpack-test",
  "version": "1.0.0",
  "description": "",
  "private": false,
  "scripts": {
    "wdserver": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "css-loader": "^3.2.0",
    "html-webpack-plugin": "^3.2.0",
    "style-loader": "^1.0.0",
    "webpack": "^4.41.1",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.2"
  }
}
// /webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')

module.exports = {
  entry: {
    main: './src/index.js'
  },
  output: {
    publicPath: '/',
    filename: '[name].[hash].js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html'
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    hot: true,
    hotOnly: true
  }
}

在 js 中使用

更改 number.js 文件中的代码,只会从页面上移除 id 为 number 的元素,然后重新执行一遍 number() 方法,不会对页面上的其它部分产生影响,也不会导致整个页面的重载

// /index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>html 模板</title>
  </head>
  <body></body>
</html>
// /src/counter.js
function counter() {
  var div = document.createElement('div')
  div.setAttribute('id', 'counter')
  div.innerText = 1
  div.onclick = function() {
    div.innerText = parseInt(div.innerText, 10) + 1
  }
  document.body.appendChild(div)
}

export default counter
// /src/number.js
function number() {
  var div = document.createElement('div')
  div.setAttribute('id', 'number')
  div.innerText = 20
  document.body.appendChild(div)
}

export default number
// /src/index.js
import counter from './counter'
import number from './number'

counter()
number()

// 相比 css 需要自己书写重载的代码,那是因为 css-loader 内部已经帮 css 写好了这部分代码
if (module.hot) {
  module.hot.accept('./number', () => {
    // 监测到代码发生变化,就会执行下面的代码
    document.body.removeChild(document.getElementById('number'))
    number()
  })
}
// /package.json
{
  "name": "webpack-test",
  "version": "1.0.0",
  "description": "",
  "private": false,
  "scripts": {
    "wdserver": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.41.1",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.2"
  }
}
// /webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')

module.exports = {
  entry: {
    main: './src/index.js'
  },
  output: {
    publicPath: '/',
    filename: '[name].[hash].js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'production',
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html'
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    hot: true,
    hotOnly: true
  }
}

09 webpack 的核心概念-使用babel处理ES6语法

babel 官网

使用 babel

点击去官网查看(选择webpack)

  1. npm i -D babel-loader @babel/core @babel/preset-env # 安装 babel-loader 和 @babel/core 和 @babel/preset-env
  • babel-loader: 是 webpack 和 babel 通讯的桥梁,使 webpack 和 babel 打通,babel-loader 并不会把 js 中的 ES6 语法转换成 ES5 语法
  • @babel/core: 是 babel 的核心语法库,它能够让 babel 识别 js 代码里面的内容并做转化
  • @babel/preset-env: 将 ES6 语法转换成 ES5 语法,它包含了所有 ES6 语法转换成 ES5 语法的翻译规则
  1. 配置
// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /node_modules/, // 不去匹配 node_modules 文件夹下的 js
      loader: 'babel-loader',
      options: {
        presets: ['@babel/preset-env']
      }
    }]
  }
}

上面的步骤,只是做了语法上的翻译(如: let/const/箭头函数/... 都会被转换),但一些新的变量和方法并没有被翻译(如: promise/.map()/...),这时就要使用 @babel/polyfill 来处理

@babel/polyfill

使用 @babel/polyfill

  1. npm i -D @babel/polyfill # 安装 @babel/polyfill
  2. import '@babel/polyfill' # 在入口文件 index.js 第一行引入 @babel/polyfill

像上面配置好之后,会发现打包后的文件特别大,因为一些没用到的 ES6 语法也被打包了进去,因此需要做如下操作

  • 参考文档
  • npm i -D core-js # 安装 core-js(v3.3.2)
  • 删除入口文件 index.js 中的 import '@babel/polyfill'
  • 配置
// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel-loader',
      options: {
        presets: [
          [
            '@babel/preset-env',
            {
              corejs: 3,
              useBuiltIns: 'usage',
              targets: { // 通过 targets 指定项目运行的环境,打包时会自动判断是否需要去解析转化代码
                chrome: '67'
              }
            }
          ]
        ]
      }
    }]
  }
}

如果写的是业务代码,可采用上面方法使用 polyfill 去打包;如果是开发组件或者库的话,可使用 plugin-transform-runtime
polyfill 会污染全局环境,plugin-transform-runtime 会以闭包的形式帮助组件去引入相关内容
@babel/plugin-transform-runtime 官方文档


10 webpack 的核心概念-打包React框架代码

@babel/preset-react 文档

// /index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>html 模板</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
// /src/index.js
import React, { Component } from 'react'
import ReactDom from 'react-dom'

class App extends Component {
  render() {
    return <div>Hello World</div>
  }
}

ReactDom.render(<App />, document.getElementById('app'))
// /.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "corejs": 3,
        "useBuiltIns": "usage",
        "targets": {
          "chrome": 67
        }
      }
    ],
    "@babel/preset-react"
  ]
}
// /package.json
{
  "name": "webpack-test",
  "version": "1.0.0",
  "description": "",
  "private": false,
  "scripts": {
    "wdserver": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.6.4",
    "@babel/polyfill": "^7.6.0",
    "@babel/preset-env": "^7.6.3",
    "@babel/preset-react": "^7.6.3",
    "@babel/runtime-corejs3": "^7.6.3",
    "babel-loader": "^8.0.6",
    "clean-webpack-plugin": "^3.0.0",
    "core-js": "^3.3.2",
    "html-webpack-plugin": "^3.2.0",
    "react": "^16.10.2",
    "react-dom": "^16.10.2",
    "webpack": "^4.41.1",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.2"
  }
}
// /webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const webpack = require('webpack')

module.exports = {
  entry: {
    main: './src/index.js'
  },
  output: {
    publicPath: './',
    filename: '[name].[hash].js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'development',
  devtool: 'cheap-module-eval-source-map',
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader'
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html'
    }),
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, 'dist')]
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    hot: true,
    hotOnly: true
  }
}

11 webpack 的高级概念-TreeShaking

  • TreeShaking 是基于 ES6 的静态引用,通过扫描所有 ES6 的 export,找出被 import 的内容并添加到最终代码中,排除不使用的代码
  • TreeShaking 只支持 ES Module 的引入方式,不支持 CommonJS 的引入方式
  • 主要用于生产环境,需要在 package.json 中配置 "sideEffects": false (对所有的模块都进行 TreeShaking 处理)
  • 如果引入的库并没有导出任何内容(如: import '@babel/polyfill'),就需要配置 "sideEffects": ["@babel/polyfill"],让 TreeShaking 不对 @babel/polyfill 进行处理
  • 如果引入样式文件(如: import './style.css'),则需配置 "sideEffects": ["*.css"]
  • 若要在开发环境使用 TreeShaking ,需在 webpack.config.js 中配置
module.exports = {
  optimization: {
    usedExports: true
  }
}

12 webpack 的高级概念-dev&prod模式的区分打包

  1. npm i -D webpack-merge # 安装 webpack-merge 模块,作用是将公共的 webpack 配置代码与开发 / 生产环境中的 webpack 配置代码进行合并

  2. /build/webpack.common.js # 存放公共的 webpack 配置代码

// 示例仅展示部分代码(下同)
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  entry: {
    main: './src/index.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html'
    }),
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: [path.resolve(process.cwd(), 'dist')] // __dirname => process.cwd()
    })
  ],
  output: {
    filename: '[name].[hash].js',
    path: path.resolve(__dirname, '../dist') // dist => ../dist
  }
}
  1. /build/webpack.dev.js # 存放开发环境的 webpack 配置代码
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')

const devConfig = {
  mode: 'development'
}
module.exports = merge(commonConfig, devConfig)
  1. /build/webpack.prod.js # 存放生产环境的 webpack 配置代码
const merge = require('webpack-merge')
const commonConfig = require('./webpack.common.js')

const prodConfig = {
  mode: 'production'
}
module.exports = merge(commonConfig, prodConfig)
  1. /package.json # 配置打包命令
{
  "scripts": {
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js"
  }
}
  1. 运行命令
  • npm run dev # 开发模式
  • npm run build # 生产模式

附录

  • process.cwd() # 执行 node 命令的文件夹地址
  • __dirname # 执行 js 的文件目录
  • path.join(path1, path2, path3, ...) # 将路径片段连接起来形成新的路径
  • path.resolve([from...], to) # 将一个路径或路径片段的序列解析为一个绝对路径,相当于执行 cd 操作

13 webpack 的高级概念-CodeSplitting

  • CodeSplitting:代码分割,代码分割和 webpack 无关
  • npm i -S lodash # 安装 lodash

手动代码分割(配置多个入口文件)

// /src/lodash.js
import _ from 'lodash'
window._ = _
// /src/index.js
console.log(_.join(['a', 'b', 'c'])) // 输出a,b,c
console.log(_.join(['a', 'b', 'c'], '***')) // 输出a***b***c
// /build/webpack.common.conf.js
module.exports = {
  entry: {
    lodash: './src/lodash.js',
    main: './src/index.js'
  }
}

自动代码分割

webpack 中的代码分割底层使用的是 SplitChunksPlugin 这个插件

webpack 中实现同步代码分割(需要配置optimization)

// /src/index.js
import _ from 'lodash'

console.log(_.join(['a', 'b', 'c'])) // 输出a,b,c
console.log(_.join(['a', 'b', 'c'], '***')) // 输出a***b***c
// /build/webpack.common.conf.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
}

webpack 中实现异步代码分割(通过 import 引入等,不需要任何配置)

// /src/index.js
function getComponent() {
  return import('lodash').then(({ default: _ }) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['hello', 'world'], '-')
    return element
  })
}

getComponent().then(element => {
  document.body.appendChild(element)
})

通过 import('lodash') 引入,分割打包后的文件名称是 [id].[hash].js,打包后文件的辨识度不高;
使用 import(/* webpackChunkName: "lodash" / 'lodash') 来为打包后的文件起别名,提升辨识度(最终生成文件名称为:vendors~lodash.[hash].js,意思是符合 vendors 组的规则,入口是main),详情可搜索查看 SplitChunksPlugin 的配置
这种方式被称为
魔法注释*,详情可查看魔法注释 Magic Comments 官网地址

注意:
如果报错“Support for the experimental syntax 'dynamicImport' isn't currently enabled”,可安装 @babel/plugin-syntax-dynamic-import 进行解决

@babel/plugin-syntax-dynamic-import 官网地址

// npm i -D @babel/plugin-syntax-dynamic-import # 安装模块包

// /.babelrc # 配置
{
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}

附录

  • 打包后输出文件的命名
// /build/webpack.common.conf.js
module.exports = {
  output: {
    filename: '[name].[hash].js', // 入口文件根据 filename 命名
    chunkFilename: '[name].chunk.js', // 非入口文件根据 chunkFilename 命名
    path: path.resolve(__dirname, '../dist')
  }
}

14 webpack 的高级概念-SplitChunksPlugin

SplitChunksPlugin 官网地址

// SplitChunksPlugin 的默认配置
// webpack.common.conf.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'async', // async:做代码分割时,只对异步代码生效;all:对同步和异步代码都生效;initial:只对同步代码生效
      minSize: 30000, // 单位:字节;当打包的库大于 minSize 时才做代码分割,小于则不做代码分割
      maxSize: 0, // 当打包的库大于 maxSize 时,尝试对其进行二次分割,一般不做配置
      minChunks: 1, // 当一个模块被用了至少 minChunks 次时,才对其进行代码分割
      maxAsyncRequests: 5, // 同时加载的模块数最多是 maxAsyncRequests 个,如果超过 maxAsyncRequests 个,只对前 maxAsyncRequests 个类库进行代码分割,后面的就不做代码分割
      maxInitialRequests: 3, // 整个网站首页(入口文件)加载的时候,入口文件引入的库进行代码分割时,最多只能分割 maxInitialRequests 个js文件
      automaticNameDelimiter: '~', // 打包生成文件名称之间的连接符
      name: true, // 打包起名时,让 cacheGroups 里的名字有效
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/, // 如果是从 node_modules 里引入的模块,就打包到 vendors 组里
          priority: -10 // 指定该组的优先级,若一个类库符合多个组的规则,就打包到优先级最高的组里
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true // 如果一个模块已经被打包过了(一个模块被多个文件引用),那么再打包的时候就会忽略这个模块,直接使用之前被打包过的那个模块
        }
      }
    }
  }
}

注:SplitChunksPlugin 上面的一些配置需要配合 cacheGroups 里的配置一起使用才能生效(如 chunks 的配置)

// webpack.common.conf.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          filename: 'vendors.js' // 配置 filename 之后,打包会以 filename 的值为文件名,生成的文件是 vendors.js
        },
        default: false
      }
    }
  }
}

15 webpack 的高级概念-LazyLoading&Chunk

LazyLoading

LazyLoading:懒加载并不是 webpack 里面的概念,而是 ES 里面的概念;什么时候执行,什么时候才会去加载对应的模块

  • 下面这种同步代码的写法,打包时将分割后的模块对应的 js 文件直接通过 script 标签在 html 中引入,页面开始加载的时候就会去加载这些 js,导致页面加载很慢
// /src/index.js
import _ from 'lodash'

document.addEventListener('click', () => {
  var element = document.createElement('div')
  element.innerHTML = _.join(['hello', 'world'], '-')
  document.body.appendChild(element)
})
  • 下面这种异步代码的写法可以实现一种懒加载的行为,在点击界面的时候才会去加载需要的模块
// /src/index.js
function getComponent() {
  return import(/* webpackChunkName: "lodash" */ 'lodash').then(
    ({ default: _ }) => {
      var element = document.createElement('div')
      element.innerHTML = _.join(['hello', 'world'], '-')
      return element
    }
  )
}

document.addEventListener('click', () => {
  getComponent().then(element => {
    document.body.appendChild(element)
  })
})

使用 ES7 的 async 和 await 后,上面代码可以改写成下面这种写法,效果等同

// /src/index.js
async function getComponent() {
  const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash')
  const element = document.createElement('div')
  element.innerHTML = _.join(['hello', 'world'], '-')
  return element
}

document.addEventListener('click', () => {
  getComponent().then(element => {
    document.body.appendChild(element)
  })
})

Chunk

打包后生成的每一个 js 文件,都是一个 chunk


16 webpack 的高级概念-打包分析

webpack 打包分析工具

webpack 打包分析工具的 GitHub 仓库地址

  1. 配置打包命令
// /package.json
{
  "scripts": {
    "build": "webpack --profile --json > stats.json"
  }
}
  1. npm run build # 运行命令
  • 打包后会在根目录生成一个 stats.json 文件,它里面包含的信息就是对打包过程的一个描述
  • 将 stats.json 上传至分析打包描述文件网址,即可查看详细的分析介绍

附录:

除了 webpack 官方提供的分析工具,还有很多其它的分析工具,可查看GUIDES/Code Splitting/Bundle Analysis

  • webpack-chart:webpack stats 可交互饼图。
  • webpack-visualizer:可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的。
  • webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展示为便捷的、交互式、可缩放的树状图形式。
  • webpack bundle optimize helper:此工具会分析你的 bundle,并为你提供可操作的改进措施建议,以减少 bundle 体积大小。

谷歌浏览器自带的覆盖率工具

  • F12 打开谷歌浏览器控制台,点击右上角的三个点,选择 More tools/coverage,点击第一个记录按钮开启捕获记录页面代码的使用率,刷新页面即可查看
  • 页面开始加载时并不会执行的代码,如果在页面加载时就让它下载下来,就会浪费页面执行的效率,利用 coverage 这个工具就可以知道哪些代码使用到了,哪些没有使用到,比如像下面这种点击交互的代码,就可以放到一个异步加载的模块里,从而提高页面的执行效率

改写前:

// /src/index.js
document.addEventListener('click', () => {
  var element = document.createElement('div')
  element.innerHTML = 'hello world'
  document.body.appendChild(element)
})

改写后:

// /src/handleClick.js
function handleClick() {
  const element = document.createElement('div')
  element.innerHTML = 'hello world'
  document.body.appendChild(element)
}
export default handleClick

// /src/index.js
document.addEventListener('click', () => {
  import('./handleClick.js').then(({ default: func }) => {
    func()
  })
})
  • 所以 webpack 做代码分割打包配置时 chunks 的默认是 async,而不是 all 或者 initial;因为 webpack 认为只有异步加载这样的组件才能真正的提高网页的打包性能,而同步的代码只能增加一个缓存,实际上对性能的提升是非常有限的
// /build/webpack.common.conf.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'async' // 默认(async)只对异步代码做分割
    }
  }
}

Preloading & Prefetching

Prefetching/Preloading modules 官网地址

  • 如果所有代码都写到异步组件里,等到事件触发时才去加载对应的模块,势必会导致操作交互的反应变慢,因此需要引入 Preloading 和 Prefetching 的概念
  • 通过魔法注释的写法去使用
/* webpackPrefetch: true */
/* webpackPreload: true */
// /src/handleClick.js
function handleClick() {
  const element = document.createElement('div')
  element.innerHTML = 'hello world'
  document.body.appendChild(element)
}
export default handleClick

// /src/index.js
document.addEventListener('click', () => {
  import(/* webpackPrefetch: true */ './handleClick.js').then(({ default: func }) => {
    func()
  })
})
  • 如上代码,在主要 js 加载完成,网络带宽有空闲的时候,会自动把 handleClick.js 加载好,再触发点击时,虽然仍会去加载 handleClick.js ,但它是从缓存中去找的
  • 区别: Prefetching 是在首页(主要的 js)加载完成,网络空闲的时候去下载异步组件交互的代码;Preloading 是和主业务文件一起加载的
  • 注意: webpackPrefetch 在某些浏览器里会有一些兼容问题

17 webpack 的高级概念-CSS文件的代码分割

  • MiniCssExtractPlugin 官网地址
  • 旧版本的 MiniCssExtractPlugin 因为不支持HMR,所以最好只在生产环境中使用,如果放在开发环境中,更改样式后需要手动刷新页面,会降低开发的效率;新版本已支持开发环境中使用HMR

使用步骤

1. 安装模块包

npm install --save-dev mini-css-extract-plugin

2. 配置

// /build/webpack.common.conf.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({})
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader, // 使用了 MiniCssExtractPlugin.loader 就不需要 style-loader 了
          'css-loader'
        ]
      }
    ]
  }
}

注意: 如果使用了 TreeShaking (排除未使用的代码)还需配置

// /package.json
{
  "sideEffects": ["*.css"]
}

3. 打包输出

// /package.json
{
  "scripts": {
    "build": "webpack --config ./build/webpack.prod.conf.js"
  }
}

npm run build

CSS 打包拓展

打包文件的命名

// /build/webpack.common.conf.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css', // 打包后的 css 如果被页面直接引用,就以 filename 的规则命名
      chunkFilename: '[name].chunk.css' // 打包后的 css 如果是间接引用的,就以 chunkFilename 的规则命名
    })
  ]
}

打包文件的压缩

  1. npm i -D optimize-css-assets-webpack-plugin # 安装模块包
  2. 引入并使用
// /build/webpack.prod.conf.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
  optimization: {
    minimizer: [new OptimizeCSSAssetsPlugin({})]
  }
}

多个 css 的打包

  • 一个入口文件引入多个 css 文件,默认将其打包合并到一个 css 里
// /src/index.js
import './index1.css'
import './index2.css'
  • 多个入口文件引入不同的 css 文件,打包默认会产生多个 css 文件,可通过配置,使其合并为一个 css 文件
// /build/webpack.prod.conf.js
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles', // 打包后的文件名
          test: /\.css$/, // 匹配所有 .css 结尾的文件,将其放到该组进行打包
          chunks: 'all', // 不管是同步还是异步加载的,都打包到该组
          enforce: true // 忽略默认的一些参数(比如minSize/maxSize/...)
        }
      }
    }
  }
}
  • 多个入口文件引入多个 css 文件的打包

根据入口文件的不同,将 css 文件打包到不同的文件里
参考Extracting CSS based on entry 官网地址


18 webpack 的高级概念-浏览器缓存

  • hash:它是工程级别的,修改任何一个文件,它的值都会改变
  • chunkhash:它会根据不同的入口文件进行依赖解析(即:同一个入口文件,对应的 css 改变了,即使对应的 js 没有改变,其 chunkhash 的值也会改变)
  • contenthash:它是针对内容级别的,只要源代码不变,它的值就不变
// /build/webpack.prod.conf.js
module.exports = {
  output: {
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].js'
  }
}

注意:

对于老版本的 webpack,即便没有对源代码做任何的变更,有可能两次打包的 contenthash 值也不一样,这是因为打包生成的文件之间存在关联,这些关联代码叫做 manifest,存在于各个文件中,可通过额外的配置,将关联代码提取出来

// /build/webpack.common.conf.js
module.exports = {
  optimization: {
    runtimeChunk: {
      name: 'runtime' // 打包后会多出一个 runtime.js 用于存储文件之间的关联代码
    }
  }
}

附录

  • 打包后文件太大,webpack 会报警告,可通过配置忽略警告
// /build/webpack.common.conf.js
module.exports = {
  performance: false
}

19 webpack 的高级概念-Shimming

自动引入依赖库

  • 有时候,我们引入了一个库,里面可能会依赖一些别的库
  • 当我们调用引入库里面的方法时,即使在当前 js 中加载了依赖库,仍然会报 xxx is not defined
  • 这时,就需要使用 webpack 自带的一个插件 ProvidePlugin
  • 它的作用是:(以: 'jquery'为例) 如果一个模块中使用了 字符串,就在该模块中自动引入 jquery 模块,然后将 jquery 模块的名字叫做 ,即自动加入这样的一行代码:import from 'jquery'
// /src/jquery.ui.js
export function ui() {
  $('body').css('background-color', _.join(['green'], ''))
}
// /src/index.js
import { ui } from './jquery.ui'
ui()
// /build/webpack.common.conf.js
const webpack = require('webpack')
module.exports = {
  plugins: [new webpack.ProvidePlugin({
    $: 'jquery',
    _: 'lodash',
    _join: ['lodash', 'join'] // 如果想直接使用 _join 替代 lodash 的 join 方法,可以这样配置
  })]
}

改变模块中的 this 指向

  • 模块中的 this 一般都指向的是模块自身,如果想改变 this 的指向,可以借助 imports-loader 模块
  1. npm i -D imports-loader # 安装模块包
  2. 使用
// /build/webpack.common.conf.js
module.exports = {
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /node_modules/,
      use: [{
        loader: 'babel-loader'
      }, {
        loader: 'imports-loader?this=>window'
      }]
    }]
  }
}
  1. 作用
    当加载一个 js 文件时,首先会走 imports-loader,它会把这个 js 文件(模块)里面的 this 改成 window,然后再交给 babel-loader 去编译

总结

以上这些更改 webpack 打包的一些默认行为,或者说实现一些 webpack 原始打包实现不了的效果,的行为都叫做 Shimming (垫片的行为)


20 webpack 的高级概念-环境变量

环境变量的使用

// /build/webpack.prod.conf.js
const prodConfig = {
  // ...
}
module.exports = prodConfig
// /build/webpack.dev.conf.js
const devConfig = {
  // ...
}
module.exports = devConfig
// /build/webpack.common.conf.js
const merge = require('webpack-merge')
const devConfig = require('./webpack.dev.conf.js')
const prodConfig = require('./webpack.prod.conf.js')
const commonConfig = {
  // ...
}
module.exports = (env) => {
  if (env && env.production) {
    return merge(commonConfig, prodConfig)
  } else {
    return merge(commonConfig, devConfig)
  }
}
// package.json
{
  "scripts": {
    "dev": "webpack-dev-server --config ./build/webpack.common.conf.js",
    "build": "webpack --env.production --config ./build/webpack.common.conf.js"
  }
}
  • 打包命令中加入 --env.production,默认会给 production 赋 true 值
  • 还可以指定具体的值,如:--env.production=abc

21 webpack 实战-Library的打包

示例代码及配置

// /src/index.js
export function add(a, b) {
  return a + b
}
export function minus(a, b) {
  return a - b
}
export function multiply(a, b) {
  return a * b
}
export function division(a, b) {
  return a / b
}
// /webpack.config.js
const path = require('path')
module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js-math.js',
    library: 'jsMath',
    libraryTarget: 'umd'
  }
}

配置详解及使用示例

配置 library: 'jsMath'

  • 打包后的 js 支持 script 标签引入使用
  • 打包生成的代码会挂载到 jsMath 这个全局变量上
// /dist/index.html
<script src="./js-math.js"></script>
<script>
  console.log(jsMath.add(2, 4)) // 6
</script>

配置 libraryTarget: 'umd'

  • u: 代表通用(universally)
  • 打包后的代码可在 ES2015/CommonJS/AMD 环境中使用
  • 不支持 script 标签直接引用使用
// ES2015 module import:
import jsMath from 'js-math'
jsMath.add(2, 3)

// CommonJS module require:
const jsMath = require('js-math')
jsMath.add(2, 3)

// AMD module require:
require(['js-math'], function(jsMath) {
  jsMath.add(2, 3)
})

libraryTarget 的一些其他值

  • libraryTarget 的值还可以配合 library 的值使用
libraryTarget: 'var' // 让 library 的值作为全局变量使用
libraryTarget: 'this' // 将 library 的值挂载到 this 对象上使用
libraryTarget: 'window' // 将 library 的值挂载到 window 对象上使用
libraryTarget: 'umd' // 使其支持在 ES2015/CommonJS/AMD 中使用

自定义库中引入其它库

  • 有时候我们会在自定义的库里面引入一些其它的库,比如:
  • import _ from 'lodash'
  • 如果别人使用我们这个库的同时,又使用了 lodash
  • 最后打包时,就会在代码中产生2份 lodash,导致重复代码
  • 解决方案:配置 externals 参数
  • Externals 官网链接
// /webpack.config.js
module.exports = {
  // externals: ['lodash'] // 表示我们的库在打包时不把 lodash 打包进去,而是让业务代码去加载 lodash

  externals: { // 详细配置
    lodash: {
      root: '_', // 表示如果 lodash 是通过 script 标签引入的,必须在页面上注入一个名为 _ 的全局变量,这样才能正确执行
      commonjs: 'lodash' // 表示通过 CommonJS 这种写法去加载时,名称必须起为 lodash,如:const lodash = require('lodash')
    }
  }
}

发布库供别人使用(未尝试)

  1. 配置入口
// /package.json
{
  "main": "./dist/js-math.js"
}
  1. npm 官网注册 npm 帐号

  2. 运行命令
    npm adduser # 添加用户信息
    npm publish # 将库上传到 npm

  3. npm install js-math # 安装使用


22 webpack 实战-PWA的打包

PWA 的介绍及使用

  • PWA:(Progressive Web Application)即使服务器挂了,依然能够访问页面
  1. npm i -D workbox-webpack-plugin # 安装模块包

  2. 配置:

// /build/webpack.prod.conf.js
const WorkboxPlugin = require('workbox-webpack-plugin')

module.exports = {
  plugins: [
    new WorkboxPlugin.GenerateSW({
      clientsClaim: true,
      skipWaiting: true
    })
  ]
}
// /src/index.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .then(registration => {
        console.log('service-worker registed')
      })
      .catch(error => {
        console.log('service-worker register error')
      })
  })
}
  1. npm run build # 打包项目
    打包后会多出两个文件:precache-manifest.js 和 service-worker.js

  2. 启动一个服务,访问打包后的项目
    断开服务,刷新浏览器,项目仍能正常访问

附录

启动一个本地服务

  1. npm i -D http-server # 安装模块包

  2. 配置命令,在 dist 目录下启动一个服务

// /package.json
{
  "scripts": {
    "httpServer": "http-server dist"
  }
}
  1. npm run httpServer # 运行命令
    打开浏览器,访问:http://127.0.0.1:8080/index.html
    注:要在访问地址后加 /index.html,否则可能会出现报错

23 webpack 实战-TypeScript的打包

TypeScript 介绍

  • TypeScript 官网
  • TypeScript 是微软推出的一个产品,它规范了一套 JavaScript 语法
  • TypeScript 是 JavaScript 的一个超集,支持 JavaScript 里面的所有语法,同时还提供了一些额外的语法
  • TypeScript 最大的优势就是可以规范我们的代码,还可以方便的对代码进行报错提示
  • 用 TypeScript 编写代码,可有效的提高 JavaScript 代码的可维护性
  • TypeScript 文件后缀一般都是 .ts 或者 .tsx

用 webpack 打包 TypeScript 代码

  1. npm i -D typescript ts-loader # 安装模块包

  2. 编写代码及配置

// /src/index.tsx
class Greeter {
  greeting: string
  constructor(message: string) {
    this.greeting = message
  }
  greet() {
    return 'Hello, ' + this.greeting
  }
}

let greeter = new Greeter('world')
// let greeter = new Greeter(123) // 由于 Greeter 中限定了数据类型为 string,这里如果传非 string 的数据,就会在代码中报错
alert(greeter.greet())
// /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: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  }
}
// /tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist", // 用 ts-loader 做 TypeScript 代码打包时,将打包生成的文件放到 ./dist 目录下(不写也行,因为 webpack.config.js 中已经配置了)
    "module": "es6", // 指的是用 ES Module 模块的引用方式(即:如果在 index.tsx 文件里引入其它模块的话,需要通过 import ... 这种方式去引入)
    "target": "es5", // 指的是打包 TypeScript 语法时,要将最终的语法转换成什么形式
    "allowJs": true // 允许在 TypeScript 语法里引入 js 模块
  }
}
  1. npm run build # 打包代码

在 TypeScript 中引入其它库(以 lodash 为例)

虽然在写 TypeScript 代码时,会有很好的错误提示,但有时在 TypeScript 代码中引入一些其它的库,调用其它库的方法时,并没有错误提示,需要执行以下步骤:

  1. npm i -D @types/lodash # 需要额外安装 @types/lodash 模块包
  2. 需要通过 import * as _ from 'lodash' 引入,而不是 import _ from 'lodash'

如果不确定是否有对应库的类型文件的支持,可以在GitHub上搜索 DefinitelyTyped,打开后下面有个 TypeSearch 的链接,去 TypeSearch 页面里搜索,如果搜索到了,就说明它有对应库的类型文件的支持,然后安装即可


24 webpack 实战-请求转发

  • devServer.proxy 官网链接
  • 使用 WebpackDevServer 实现开发环境下的请求转发
  • 依赖 WebpackDevServer,需要安装 webpack-dev-server 模块
// /src/index.js // 使用 axios 模拟请求
import axios from 'axios'
axios.get('/api/data.json').then(res => {
  console.log(res)
})
// /webpack.config.js
module.exports = {
  devServer: {
    proxy: {
      // '/api': 'http://...' // 简写,如果请求是以 /api 开头的,就将其代理到 http://... 进行请求
      '/api': {
        target: 'http://...', // 若请求地址以 /api 开头,将其代理到 http://... 进行请求
        secure: false, // 如果请求地址是 https 的话,需要配置此项
        pathRewrite: { // 对一些请求路径的重写
          'data.json': 'data-test.json'
        },
        changeOrigin: true, // 可以帮助我们改变请求里的 origin,跳过一些服务端的 origin 验证
        headers: { // 请求转发时改变请求头,模拟一些数据
          host: 'www...',
          cookie: '123...'
        }
      }
    }
  }
}

25 webpack 实战-单页面应用的路由

// /src/home.js
import React, { Component } from 'react'

class Home extends Component {
  render() {
    return <div>HomePage</div>
  }
}

export default Home
// /src/list.js
import React, { Component } from 'react'

class List extends Component {
  render() {
    return <div>ListPage</div>
  }
}

export default List
// /src/index.js
import React, { Component } from 'react' // 需要安装 react 库
import { BrowserRouter, Route } from 'react-router-dom' // 需要安装 react-router-dom 库
import ReactDom from 'react-dom' // 需要安装 react-dom 库
import Home from './home.js'
import List from './list.js'

class App extends Component {
  render() {
    return (
      <BrowserRouter>
        <div>
          <Route path="/" exact component={Home} />
          <Route path="/list" component={List} />
        </div>
      </BrowserRouter>
    )
  }
}

ReactDom.render(<App />, document.getElementById('root')) // 需要在 html 中写一个 id 为 root 的容器
// /webpack.config.js
module.exports = {
  devServer: {
    historyApiFallback: true // 只需配置该参数,即可通过不同的路由加载不同的 js
    // 注意:这种方法只适用于开发环境中,上线使用需要后端做路由映射处理
  }
}

26 webpack 实战-ESLint的配置

ESLint的使用

  1. npm i -D eslint # 安装模块包
  2. npx eslint --init # 初始 eslint,根据项目实际情况做一些选择,生成 eslint 配置文件 .eslintrc.js
// /.eslintrc.js
module.exports = {
  env: { // 指定代码的运行环境。不同的运行环境,全局变量不一样,指明运行环境这样 ESLint 就能识别特定的全局变量。同时也会开启对应环境的语法支持
    browser: true,
    es6: true,
  },
  extends: [
    'plugin:vue/essential',
    'airbnb-base',
  ],
  globals: {
    Atomics: 'readonly',
    SharedArrayBuffer: 'readonly',
  },
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module',
  },
  plugins: [
    'vue',
  ],
  rules: { // 这里可以对规则进行细致的定义,覆盖 extends 中的规则
  },
};
  1. 执行检测
  • npx eslint ./src # 检测 ./src 目录下的所有文件是否符合规则
  • npx eslint ./src/index.js # 检测某一个文件是否符合规则

其它

  • 初始化时,只有选择"To check syntax, find problems, and enforce code style"时,才可以选择 Airbnb, Standard, Google 标准
  • eslint 配置文件(.eslintrc.js)的格式最好选择 JavaScript 格式,因为 json 格式不支持代码注释,并且在需要根据环境变量来做不同情况处理时十分无力
  • 执行 npx eslint ... 检测命令时,加上 --fix (即:npx eslint --fix ...)可以自动修正一些代码风格的问题(如:代码后加分号等),但代码错误的问题不会修改
  • 若使用 VSCode 开发工具,可安装 ESLint 插件,安装后会在代码中自动提示错误信息

在 webpack 中配置 ESLint

// /webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: ['babel-loader', {
          loader: 'eslint-loader', // 在 babel-loader 处理之前先用 eslint-loader 检测一下
          options: {
            fix: true, // 作用同 npx eslint --fix ...
            cache: true // 可以降低 ESLint 对打包过程性能的损耗
            // force: 'pre' // 不管 eslint-loader 放在什么位置,强制它最先执行
          }
        }]
      }
    ]
  },
  devServer: {
    overlay: true // 配置此项后,【开发环境】在浏览器打开项目时,eslint 检查的一些报错信息就会以浮层的形式在浏览器中展示
  }
}
  • 真实项目一般不会在 webpack 中配置 eslint-loader,因为它会降低打包速度
  • 一般在项目提交时去做 ESLint 的检测,检测通过才允许提交

27 webpack 实战-性能优化

提升打包速度

1. 跟上技术的迭代(webpack/node/npm/yarn/...)

  • 尽可能使用新版本的工具,因为新版本中做了更多的优化

2. 在尽可能少的模块上应用 loader

  • 通过配置 exclude / include 减少 loader 的使用
// /webpack.config.js
const path = require('path')

module.exports = {
  module: {
    rules: [{
      exclude: /node_modules/ // 排除应用规则的目录
      // include: path.resolve(__dirname, './src') // 限定应用规则的目录
    }]
  }
}

3. Plugin 尽可能精简并确保可靠

  • 尽量使用官方推荐的插件,官方的优化的更好

4. resolve 参数合理配置

// /webpack.config.js
module.exports = {
  resolve: {
    extensions: ['.js', '.jsx'], // 当我们引入一个组件,未指定后缀时(如:import Child from './child/child'),它会自动先去找 ./child/child.js,如果没有,再去找 ./child/child.jsx,合理的配置可减少查找匹配的次数,降低性能损耗

    mainFiles: ['index', 'child'], // 配置该项后,当我们引入一个文件夹路径时(如:import Child from './child/'),它就会自动先去找该文件夹下的 index,如果没有,再去找 child。同上,该配置不易过多,否则影响性能

    alias: { // 配置别名
      child: path.resolve(__dirname, './src/child') // 使用时就可以这样写:import Child from 'child'
    }
  }
}

5. 使用 DllPlugin 提高打包速度

5.1 单独打包库文件

  • 以 lodash 和 jquery 为例:
// /build/webpack.dll.js
const path = require('path')

module.exports = {
  mode: 'production',
  entry: {
    vendors: ['lodash', 'jquery']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]' // 打包生成一个库,并暴露在全局变量 [name](即:vendors)中
  }
}
// /package.json
{
  "scripts": {
    "build:dll": "webpack --config ./build/webpack.dll.js"
  },
  "dependencies": {
    "jquery": "^3.4.1",
    "lodash": "^4.17.15"
  }
}
  • npm run build:dll # 运行打包命令,输出 /dll/vendors.dll.js 文件
  • /dll/vendors.dll.js 即是打包所有库产生的新的库文件,它里面包含了 lodash 和 jquery 的源码

5.2 利用插件将单独打包产生的新的库文件引入到生产打包的代码中

  • npm i -D add-asset-html-webpack-plugin # 安装模块包并配置
// /webpack.config.js
const path = require('path')
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')

module.exports = {
  plugins: [
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, './dll/vendors.dll.js') // 指的是要往 HtmlWebpackPlugin 生成的 index.html 里加的内容
    })
  ]
}
// /package.json
{
  "scripts": {
    "build": "webpack"
  }
}
  • npm run build # 运行打包命令,会复制一份 vendors.dll.js 到 /dist/ 目录下,并在 /dist/index.html 文件中引入 vendors.dll.js

至此,第三方模块只打包一次,并引入生产打包代码中的目标已经实现了
但是 /src/index.js 中 import _ from 'lodash' 使用的还是 node_modules 里面的库
接下来需要实现的是:引入第三方模块的时候,让它从 dll 文件里引入,而不是从 node_modules 里引入

5.3 构建映射,使用模块时,让其从 dll 文件里加载

// /build/webpack.dll.js
// 通过该配置文件打包会生成类似于库的打包结果

const path = require('path')
const webpack = require('webpack')

module.exports = {
  plugins: [
    new webpack.DllPlugin({
      // 使用 webpack 自带的插件对打包产生的库文件进行分析
      // 把库里面一些第三方模块的映射关系放到 path 对应的文件里

      name: '[name]', // 暴露出的 DLL 函数的名称
      path: path.resolve(__dirname, '../dll/[name].manifest.json') // 分析结果文件输出的绝对路径
    })
  ]
}
  • npm run build:dll # 执行打包 dll 命令,产生 /dll/[name].dll.js 新的库文件和 /dll/[name].manifest.json 映射文件
  • 有了这个映射文件,打包业务代码时,就会对源代码进行分析
  • 如果分析出使用的内容是在 /dll/[name].dll.js 里,那么,它就会使用 /dll/[name].dll.js 里的内容,就不会去 node_modules 里引入模块了
  • 接下来就是:结合全局变量和生成的 /dll/[name].manifest.json 映射文件进行 webpack 的配置
// /webpack.config.js
module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, './dll/vendors.manifest.json')
    })
  ]
}
  • npm run build # 运行打包命令,打包耗时就会减少了

5.4 打包生成多个新的库文件

  • 配置多个入口文件
// /build/webpack.dll.js
module.exports = {
  entry: {
    vendors: ['lodash', 'jquery'],
    react: ['react', 'react-dom']
  }
}
  • 结合 5.3 的配置,此时打包输出的文件有:
    /dll/vendors.dll.js
    /dll/vendors.manifest.json
    /dll/react.dll.js
    /dll/react.manifest.json

  • 然后配置 /webpack.config.js

// /webpack.config.js
module.exports = {
  plugins: [
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, './dll/vendors.dll.js')
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, './dll/vendors.manifest.json')
    }),
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, './dll/react.dll.js')
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, './dll/react.manifest.json')
    })
  ]
}
  • 最后执行 npm run build 打包即可

注:如果打包生成的 dll 文件有很多,就需要在 /webpack.config.js 中添加很多的 plugin,为了简化代码,可以借助 node 去分析 dll 文件夹下的文件,循环处理,代码如下:

// /webpack.config.js
const fs = require('fs') // 借助 node 中的 fs 模块去读取 dll 文件夹

const plugins = [ // 初始存入一些基础的 plugin
  new HtmlWebpackPlugin({
    template: './src/index.html'
  })
]

const files = fs.readdirSync(path.resolve(__dirname, './dll'))
files.forEach(file => {
  if (/.*\.dll.js/.test(file)) {
    plugins.push(new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, './dll', file)
    }))
  }
  if (/.*\.manifest.json/.test(file)) {
    plugins.push(new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, './dll', file)
    }))
  }
})

module.exports = {
  plugins
}
  • 配置完成后,需再次运行 npm run build:dll 生成 dll 文件和映射文件
  • 最后再次执行 npm run build 打包即可

6. 控制包文件大小

  • 不要引入一些未使用的模块包
  • 配置 Tree-Shaking,打包时,不打包一些引入但未使用的模块
  • 配置 splitChunks,对代码进行合理拆分,将大文件拆成小文件打包

7. thread-loader/parallel-webpack/happypack 多进程打包

  • webpack 默认是通过 nodeJs 来运行的,是单进程的打包
  • 可以使用 thread-loader / parallel-webpack / happypack 这些技术,配置多进程打包

8. 合理使用 sourceMap

  • 打包生成的 sourceMap 越详细,打包的速度就越慢,可根据不同的环境配置不同的 sourceMap

9. 结合 stats 分析打包结果

  • 根据打包分析的结果,做对应的优化

10. 开发环境内存编译

  • 开发环境使用 webpack-dev-server,启动服务后,会将编译生成的文件放到内存中,而内存的读取速度远远高于硬盘的读取速度,可以让我们在开发环境中,webpack 性能得到很大的提升

11. 开发环境无用插件剔除

  • 例如:开发环境无需对代码进行压缩等

28 webpack 实战-多页面打包配置

多页面打包配置

  1. 新建多个页面的 js
// /src/index.js
console.log('home page')

// /src/list.js
console.log('list page')
  1. 配置 webpack
// /build/webpack.common.conf.js
module.exports = {
  entry: { // 配置多个入口文件
    main: './src/index.js',
    list: './src/list.js'
  },
  plugins: [
    // 配置多个打包输出页面
    new HtmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'index.html',
      chunks: ['runtime', 'vendors', 'main'] // 不同的页面引入不同的入口文件(若有 runtime 或者 vendors 就引入,没有就不写)
    }),
    new HtmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'list.html',
      chunks: ['runtime', 'vendors', 'list']
    })
  ]
}
  1. npm run build # 执行打包后输出的 index.html 中会引入 main.js,list.html 中会引入 list.js

如上,如果每增加一个页面,就手动增加代码的话,就会导致大量重复代码,下面开始对打包配置代码进行优化:

优化多页面打包配置代码

// /build/webpack.common.conf.js
const fs = require('fs')

const makePlugins = configs => { // 自定义方法 makePlugins,用于动态生成 plugins
  const plugins = [
    // 初始可以存一些基本的 plugin,如:CleanWebpackPlugin
  ]

  // 根据不同的入口文件,生成不同的 html
  // Object.keys() 方法会返回一个由给定对象枚举属性组成的数组
  Object.keys(configs.entry).forEach(item => {
    plugins.push(new HtmlWebpackPlugin({
      template: 'src/index.html',
      filename: `${item}.html`,
      chunks: [item]
    }))
  })

  // 动态添加并使用打包生成的一些第三方 dll 库
  const files = fs.readdirSync(path.resolve(__dirname, '../dll'))
  files.forEach(file => {
    if (/.*\.dll.js/.test(file)) {
      plugins.push(new AddAssetHtmlWebpackPlugin({
        filepath: path.resolve(__dirname, '../dll', file)
      }))
    }
    if (/.*\.manifest.json/.test(file)) {
      plugins.push(new webpack.DllReferencePlugin({
        manifest: path.resolve(__dirname, '../dll', file)
      }))
    }
  })

  return plugins
}

const configs = {
  // 将 module.exports 导出的一堆配置放到变量 configs 里
  entry: {
    index: './src/index.js',
    list: './src/list.js'
  }
  // ...
  // 这里不写 plugins,通过一个方法去生成 plugins
}

configs.plugins = makePlugins(configs) // 调用 makePlugins 自定义的方法,生成 plugins

module.exports = configs // 导出重组好的 configs
  • 如需增加页面,只要多配置一个入口文件即可

29 webpack 底层原理-编写Loader

  • 实际上 loader 就是一个函数,这个函数可接收一个参数,这个参数指的就是引入文件的源代码
  • 注意: 这个函数不能写成箭头函数,因为要用到 this,webpack 在调用 loader 时,会把这个 this 做一些变更,变更之后,才能用 this 里面的方法,如果写成箭头函数,this 指向就会有问题

如何编写一个 Loader

// /src/index.js
console.log('hello world !')
// /loaders/replaceLoader.js
module.exports = function(source) {
  return source.replace('hello', '你好') // 对源代码执行一个替换
}
// /package.json
{
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^4.29.0",
    "webpack-cli": "^3.2.1"
  }
}
// /webpack.config.js
const path = require('path')

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  module: {
    rules: [{
      test: /\.js$/,
      use: [
        path.resolve(__dirname, './loaders/replaceLoader.js')
      ]
    }]
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}
  • npm run build # 打包输出,运行输出文件即可查看打印的‘hello’被替换成了‘你好’,这就是一个简单的 loader

在 /loaders/replaceLoader.js 中,除了通过 return 返回处理后的源代码之外,还可以使用 this.callback 做返回处理

// /loaders/replaceLoader.js
module.exports = {
  const result = source.replace('hello', '你好')
  this.callback(null, result)
}

Loader 配置传参

// /loaders/replaceLoader.js
module.exports = function(source) {
  // 可通过 this.query 获取使用 loader 时 options 里面传递的配置
  console.log(this.query) // { name: 'xiaoli' }
  return source.replace('hello', '你好')
}
// /webpack.config.js
const path = require('path')

module.exports = {
  module: {
    rules: [{
      test: /\.js$/,
      use: [{
        loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
        options: {
          name: 'xiaoli'
        }
      }]
    }]
  }
}
  • 有时候传递过来的参数会比较诡异(比如传的是对象,接收的可能是字符串),所以官方推荐使用 loader-utils 模块去分析传递的内容

使用 loader-utils 分析 loader 配置

  • npm i -D loader-utils # 安装模块包
  • 使用:
// /loaders/replaceLoader.js
const loaderUtils = require('loader-utils')

module.exports = {
  const options = loaderUtils.getOptions(this)
  console.log(options) // { name: 'xiaoli' }
}

loader 里执行异步操作

如果 loader 里调用一些异步的操作(比如延迟 return),打包就会报错,说 loader 没有返回内容,需要使用 this.async()

// /loaders/replaceLoaderAsync.js
module.exports = function(source) {
  const callback = this.async()
  setTimeout(() => {
    const result = source.replace('hello', '你好')
    callback(null, result) // 这样调用的 callback 实际上就是 this.callback()
  }, 1000)
}

多个 loader 的使用

// /loaders/replaceLoaderAsync.js
module.exports = function(source) {
  const callback = this.async()
  setTimeout(() => {
    const result = source.replace('hello', '你好')
    callback(null, result)
  }, 1000)
}
// /loaders/replaceLoader.js
module.exports = function(source) {
  const result = source.replace('world', '世界')
  this.callback(null, result)
}
// /webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.js$/,
      use: [{
        loader: path.resolve(__dirname, './loaders/replaceLoader.js')
      }, {
        loader: path.resolve(__dirname, './loaders/replaceLoaderAsync.js')
      }]
    }]
  }
}

简化 loader 的引入方式

// /webpack.config.js
module.exports = {
  resolveLoader: {
    // 当你引入一个 loader 的时候,它会先到 node_modules 里面去找,如果找不到,再去 loaders 目录下去找
    modules: ['node_modules', './loaders']
  },
  module: {
    rules: [{
      test: /\.js$/,
      use: [{
        loader: 'replaceLoader'
      }, {
        loader: 'replaceLoaderAsync'
      }]
    }]
  }
}

30 webpack 底层原理-编写Plugin

loader 和 plugin 的区别:

  • loader 的作用是:帮我们去处理模块,当我们在源代码里面去引入一个新的 js 文件(或其它格式文件)的时候,可以借助 loader 处理引用的文件
  • plugin 的作用是:当我们在打包时,在某些具体时刻上(比如打包结束自动生成一个 html 文件,就可以使用 html-webpack-plugin 插件)去做一些处理
  • loader 是一个函数
  • plugin 是一个类

一个简单 plugin 的编写及使用

// /plugins/copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
  // 构造函数
  constructor() {
    console.log('插件被使用了')
  }

  // 当调用插件时,会执行 apply 方法,该方法接收一个参数 compiler,可以理解为 webpack 的实例
  apply(compiler) {}
}

module.exports = CopyrightWebpackPlugin
// /src/index.js
console.log('hello world !')
// /package.json
{
  "name": "plugin",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10"
  }
}
// /webpack.config.js
const path = require('path')
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin.js')

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  plugins: [new CopyrightWebpackPlugin()],
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}
  • npm run build # 运行打包命令,即可在命令行中查看到“插件被使用了”的输出信息

plugin 传参

// /webpack.config.js
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin.js')

module.exports = {
  plugins: [new CopyrightWebpackPlugin({
    name: 'li' // 这里传递参数
  })]
}
// /plugins/copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
  constructor(options) {
    // 通过 options 接收参数
    console.log(options)
  }

  apply(compiler) {}
}

module.exports = CopyrightWebpackPlugin

打包结束时刻生成额外的文件

  • Compiler Hooks 官网链接
  • compiler.hooks 里面有一些类似 vue 生命周期函数的东西(是在特定的时刻,会自动执行的钩子函数)
// /plugins/copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
  apply(compiler) {
    // emit 是指当你把打包的资源放到目标文件夹的时刻
    compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, callback) => {
      console.log('插件执行了')
      callback()
    })
  }
}

module.exports = CopyrightWebpackPlugin
  • 至此,在打包的指定时刻运行代码已经实现
  • 接下来在指定时刻,向打包内容里增加文件
// /plugins/copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, callback) => {
      // 打包生成的所有内容是存放在 compilation.assets 里面的
      // 在 emit 时刻的时候,向打包生成的内容里增加一个 copyright.txt 文件
      compilation.assets['copyright.txt'] = {
        // 文件里的内容
        source: function() {
          return 'copyright text ...'
        },
        // 文件的大小
        size: function() {
          return 18
        }
      }
      callback()
    })
  }
}

借助 node 进行调试

  • --inspect # 开启 node 的调试工具
  • --inspect-brk # 在 webpack.js 的第一行上打个断点
  • 直接运行 node node_modules/webpack/bin/webpack.js 就相当于运行 webpack
// /package.json
{
  "scripts": {
    "debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js"
  }
}
  • npm run debug 运行后,打开浏览器控制台,点击左上角绿色的图标,即可进入 node 的调试

附录

其它的打包时刻

  • done (异步时刻)表示打包完成

  • compile (同步时刻)

  • 同步时刻是 tap 且没有 callback

  • 异步时刻是 tapAsync 有 callback

compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
  console.log('compile 时刻执行')
})

31 webpack 底层原理-Bundler源码编写-模块分析

1. 读取项目的入口文件

2. 分析入口文件里的代码

// /src/word.js
export const word = 'hello'
// /src/message.js
import { word } from './word.js'
// 需要写 .js 后缀,因为没有使用 webpack

const message = `say ${word}`

export default message
// /src/index.js
import message from './message.js'

console.log(message)

npm i @babel/parser # 作用是分析代码,产生抽象语法树

npm i @babel/traverse # 作用是帮助我们快速找到 import 节点

// /bundler.js
// 此文件就是我们要做的打包工具
// 打包工具是用 nodeJs 来编写的

// node 的一个用于读取文件的模块
const fs = require('fs')
const path = require('path')
// 使用 babelParser 分析代码,产生抽象语法树
const parser = require('@babel/parser')
// 默认导出的内容是 ESModule 的导出,如果想用 export default 导出内容,需要在后面加个 .default
const traverse = require('@babel/traverse').default

const moduleAnalyser = filename => {
  // 以 utf-8 编码读取入口文件的内容
  const content = fs.readFileSync(filename, 'utf-8')
  // console.log(content)

  // 分析文件内容,输出抽象语法树
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  // ast.program.body 即是文件内容中的节点
  // console.log(ast.program.body)

  const dependencies = {}
  // 对抽象语法树进行遍历,找出 Node.type === 'ImportDeclaration' 的元素,并做处理
  traverse(ast, {
    ImportDeclaration({ node }) {
      // console.log(node)
      const dirname = path.dirname(filename)
      const newFile = './' + path.join(dirname, node.source.value)
      dependencies[node.source.value] = newFile
    }
  })
  // console.log(dependencies)

  // 将入口文件和对应依赖返回出去
  return {
    filename, // 入口文件
    dependencies // 入口文件里的依赖
  }
}

// 传入口文件,调用方法
moduleAnalyser('./src/index.js')
  • babel-core 官网链接
  • babel-preset-env 官网链接
  • 对代码分析完成之后还需要将 ES6 代码转化成浏览器可以运行的代码
  • npm i @babel/core # 安装 @babel/core
  • npm i @babel/preset-env # 安装 @babel/preset-env
  • babelCore 里的 transformFromAst() 方法,可以将 ast 抽象语法树转化成浏览器可以运行的代码
// /bundler.js
const babel = require('@babel/core')

const moduleAnalyser = filename => {
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
  })
  // code 就是浏览器可以运行的代码
  return {
    code
  }
}

// 分析转化之后的结果
const moduleInfo = moduleAnalyser('./src/index.js')
console.log(moduleInfo)

附录

  • 在命令行高亮显示代码

npm i cli-highlight -g // 安装 cli-highlight
node bundler.js | highlight // 运行时在后面加上 | highlight


32 webpack 底层原理-Bundler源码编写-DependenciesGraph

  • 在上一节代码的基础上继续编写
// /bundler.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })

  const dependencies = {}
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename)
      const newFile = './' + path.join(dirname, node.source.value)
      dependencies[node.source.value] = newFile
    }
  })

  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
  })

  return {
    filename,
    dependencies,
    code
  }
}

const makeDependenciesGraph = entry => {
  const entryModule = moduleAnalyser(entry)
  const graphArray = [ entryModule ]

  // 循环遍历依赖中的依赖
  for(let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i]
    const { dependencies } = item
    if (dependencies) {
      for(let j in dependencies) {
        graphArray.push(
          moduleAnalyser(dependencies[j])
        )
      }
    }
  }

  // 将遍历后的依赖数组转化成对象的形式
  const graph = {}
  graphArray.forEach(item => {
    graph[item.filename] = {
      dependencies: item.dependencies,
      code: item.code
    }
  })
  return graph
}

// 最终分析生成的代码和依赖信息
const graphInfo = makeDependenciesGraph('./src/index.js')
console.log(graphInfo)
  • 接下来转到下一节生成代码

33 webpack 底层原理-Bundler源码编写-生成代码

  • 在上一节代码的基础上继续编写
// /bundler.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const moduleAnalyser = filename => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })

  const dependencies = {}
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename)
      const newFile = './' + path.join(dirname, node.source.value)
      dependencies[node.source.value] = newFile
    }
  })

  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
  })

  return {
    filename,
    dependencies,
    code
  }
}

const makeDependenciesGraph = entry => {
  const entryModule = moduleAnalyser(entry)
  const graphArray = [ entryModule ]

  for(let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i]
    const { dependencies } = item
    if (dependencies) {
      for(let j in dependencies) {
        graphArray.push(
          moduleAnalyser(dependencies[j])
        )
      }
    }
  }

  const graph = {}
  graphArray.forEach(item => {
    graph[item.filename] = {
      dependencies: item.dependencies,
      code: item.code
    }
  })
  return graph
}

const generateCode = entry => {
  const graph = JSON.stringify(makeDependenciesGraph(entry))
  return `
    (function(graph) {
      function require(module) {
        function localRequire(relativePath) {
          return require(graph[module].dependencies[relativePath])
        }
        var exports = {}
        (function(require, exports, code) {
          eval(code)
        })(localRequire, exports, graph[module].code)
        return exports
      }
      require('${entry}')
    })(${graph})
  `
}

const code = generateCode('./src/index.js')
console.log(code)
  • node bundler # 运行编译输出可以在浏览器中运行的代码如下:
  • 【注:】在命令行复制输出的代码到浏览器控制台运行时,需要检查一下是否有不正确的换行,不同的命令行工具可能导致一些不正确的换行,直接复制到浏览器运行会导致报错(Uncaught SyntaxError: Invalid or unexpected token)
(function(graph) {
  function require(module) {
    function localRequire(relativePath) {
      return require(graph[module].dependencies[relativePath]);
    };
    var exports = {};
    (function(require, exports, code) {
      eval(code);
    })(localRequire, exports, graph[module].code);
    return exports;
  };
  require('./src/index.js');
})({"./src/index.js":{"dependencies":{"message.js":"./src\\message.js"},"code":"\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);"},"./src\\message.js":{"dependencies":{"./word.js":"./src\\word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"say \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"},"./src\\word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.word = void 0;\nvar word = 'hello';\nexports.word = word;"}});
  • 运行输出:"say hello"

34 脚手架工具配置分析-CreateReactApp&VueCLI3

CreateReactApp

  • create-react-app 官网链接
  • npx create-react-app my-app # 创建项目
  • cd my-app # 进入项目文件夹
  • npm start # 启动项目
  • npm run eject # 将 react 隐藏的一些配置显示出来(此操作不可逆!)

VueCLI3

  • VueCLI3 官网链接
  • npm i -g @vue/cli # 安装 VueCLI
  • vue create my-project # 创建项目
  • cd my-project # 进入项目文件夹
  • npm run serve # 启动项目
  • /vue.config.js # 可在根目录创建 vue.config.js 然后根据官网文档书写 webpack 的相关配置

本作品系原创 采用《署名-非商业性使用-禁止演绎4.0 国际》许可协议

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容