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

01 webpack 初探-导学

传统编程的弊端

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

// /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(模块打包工具) 编程

解决传统编程的弊端html

  • 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

建立文件写代码:vue

// /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 文件node

// /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 如何打包其它不一样类型的文件react

打包图片类型的文件

file-loader

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

// 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

// 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-loaderios

打包样式文件

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

打包 css / sass

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

// 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 的相关配置
相关文章
相关标签/搜索