实例带你入门并掌握Webpack4(实战篇一)

下篇 实例带你入门并掌握Webpack4(实战篇二)css

下篇 实例带你入门并掌握Webpack4(实战篇三)html

原做者:Zsh前端

前言

  1. 搭建项目并打包 JS 文件
  2. 生产和开发模式
  3. 覆盖默认 entry/output
  4. 用 Babel 7 转译 ES6
  5. Code Splitting
  6. Lazy Loading、Prefetching
  7. 自动生成 HTML 文件
  8. 处理 CSS/SCSS 文件

1. 搭建项目并打包 JS 文件

demo1 源码地址vue

建立空文件夹,经过运行如下命令初始化 package.jsonnode

npm init -y
复制代码

npm init 用来初始化生成一个新的 package.json 文件。它会向用户提问一系列问题,若是你以为不用修改默认配置,一路回车就能够了。 若是使用了 -y(表明 yes),则跳过提问阶段,直接生成一个新的 package.json 文件。webpack

引入 webpack4:git

npm i webpack --save-dev
复制代码

还须要 webpack-cli ,做为一个单独的包引入,若是不装 webpack-cli 是没法在命令行里使用 webpack 的es6

此项目 webpack 版本以下:github

"webpack": "^4.29.6",
"webpack-cli": "^3.2.3"
复制代码

如今打开 package.json 并添加一个 build(构建) 脚本:web

尝试运行看看会发生什么:

npm run build
复制代码
复制代码

在 webpack4 之前的版本中,必须在名为 webpack.config.js 的配置文件中 经过 entry 属性定义 entry point(入口点),就像这样:

从 webpack4 开始,再也不必须定义 entry point(入口点) :它将默认为 ./src/index.js

测试这个新功能,首先建立 ./src/index.js 文件

再运行 npm run build 试试

打包成功,并在当前的根目录中获得打包后的文件夹,也就是 dist 文件夹

它将查找 ./src/index.js 做为默认入口点。 并且,它会在 ./dist/main.js 中输出模块包,目前代码量小,能够格式化看效果

至此,打包 JS 结束

参考:webpack 官网入门

2.生产和开发模式

demo2 源码地址

拥有 2 个配置文件在 webpack 中是的常见模式。

一个典型的项目可能有:

  • 用于开发的配置文件,配置热更新、跨域配置、端口设置等
  • 用于生产的配置文件,配置 js 压缩、代码拆分等

虽然较大的项目可能仍然须要 2 个配置文件,但在 webpack4 中,你能够在没有一行配置的状况下完成

webpack4 引入了 production(生产) 和 development(开发) 模式。

细心的朋友会发如今 npm run build 打包后会有一段报警提示

'mode' 选项还没有设置,webpack 将回退到 'production'。 将 “mode” 选项设置为 'development' 或 'production' 以启用每一个环境的默认值。您还能够将其设置为 'none' 以禁用任何默认行为。 了解更多

  1. 打开 package.json 并填充 script 部分,以下所示:

    "dev": "webpack --mode development", "build": "webpack --mode production" 复制代码

  2. 运行 npm run dev

打开 ./dist/main.js 文件,是一个 bundle(包) 文件,并无压缩!

  1. 运行 npm run build

能够看到 ./dist/main.js 文件已经被压缩了

其实在终端里也能发现,看构建完的大小, dev 模式下文件大小是 3.8 KB, prod 模式下文件大小仅为 960 字节

production mode(生产模式) 能够开箱即用地进行各类优化。 包括压缩,做用域提高,tree-shaking 等。

另外一方面,development mode(开发模式) 针对速度进行了优化,仅仅提供了一种不压缩的 bundle

在 webpack4 中,能够在没有一行配置的状况下完成任务! 只需定义 –mode 参数便可得到全部内容!

3.覆盖默认 entry/output

demo3 源码地址

1. 检验 webpack 规范支持

webpack 支持 ES6, CommonJS, AMD 规范

建立 vendor 文件夹,其中 minus.js、multi.js 和 sum.js 分别用 CommonJS、AMD 和 ES6 规范编写

// minus.js
module.exports = function(a, b) {
  return a - b
}

// multi.js
define(function(require, factory) {
  'use strict'
  return function(a, b) {
    return a * b
  }
})

// sum.js
export default function(a, b) {
  return a + b
}
复制代码
复制代码

app.js 文件中引入以上三个 js 文件

/**
 * webpack 支持 ES六、CommonJs 和 AMD 规范
 */

// ES6
import sum from './vendor/sum'
console.log('sum(1, 2) = ', sum(1, 2))

// CommonJs
var minus = require('./vendor/minus')
console.log('minus(1, 2) = ', minus(1, 2))

// AMD
require(['./vendor/multi'], function(multi) {
  console.log('multi(1, 2) = ', multi(1, 2))
})
复制代码
复制代码

2. 编写配置文件覆盖 entry/output

webpack.config.js 是 webpack 默认的配置文件名,在根目录下建立

const path = require('path')

module.exports = {
  entry: {
    app: './app.js' // 须要打包的文件入口
  },
  output: {
    publicPath: __dirname + '/dist/', // js 引用的路径或者 CDN 地址
    path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录
    filename: 'bundle.js' // 打包后生产的 js 文件
  }
}
复制代码
复制代码

path.resolve() 方法会把一个路径或路径片断的序列解析为一个绝对路径。

__dirname: 当前模块的文件夹名称。

可使用 console.log 输出一下就明白了

const path = require('path')

console.log('__dirname: ', __dirname)
console.log('path.resolve: ', path.resolve(__dirname, 'dist'))

module.exports = {
  entry: {
    app: './app.js' // 须要打包的文件入口
  },
  output: {
    publicPath: __dirname + '/dist/', // js 引用的路径或者 CDN 地址
    path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录
    filename: 'bundle.js' // 打包后生产的 js 文件
  }
}
复制代码
复制代码

执行 npm run build 打包 js 文件

会发现生成了 dist 文件夹,并生成了两个打包后的文件

这跟 AMD 的引入方式有关,若是在 app.js 中注释掉 AMD 的写法,则只会打包出一个 bundle.js 文件

在实际写代码的时候,最好使用 ES6 和 CommonJS 的规范来写

当你注释 AMD 后,打包完 dist 中有多个文件,这是由于打包的时候,没有先删除 dist 文件,再打包,咱们须要使用一个插件来帮咱们实现,GitHub 连接:clean-webpack-plugin

① 安装插件

npm install clean-webpack-plugin --save-dev
复制代码
复制代码

② 修改 webpack 配置文件

const path = require('path')

const CleanWebpackPlugin = require('clean-webpack-plugin')

module.exports = {
  entry: {
    app: './app.js' // 须要打包的文件入口
  },
  output: {
    publicPath: __dirname + '/dist/', // js 引用的路径或者 CDN 地址
    path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录
    filename: 'bundle.js' // 打包后生产的 js 文件
  },
  plugins: [
    new CleanWebpackPlugin() // 默认状况下,此插件将删除 webpack output.path目录中的全部文件,以及每次成功重建后全部未使用的 webpack 资产。
  ]
}
复制代码
复制代码

注意!!!若是安装的 clean-webpack-plugin 是 3.0 版本的,配置要更改成:

// common js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
复制代码
复制代码

以后再执行 npm run build 就能够了

打包后的 js 文件会按照咱们的配置放在 dist 目录下,建立一个 html 文件,引用打包好的 js 文件,打开 F12 就能看到效果了

参考文章

webpack4 系列教程 (一): 打包 JS

Webpack4 教程:从零配置到生产模式

4.用 Babel 7 转译 ES6

demo4 源码地址

(一) 了解 Babel 及生态

现代 Javascript 主要是用 ES6 编写的。但并不是每一个浏览器都知道如何处理 ES6。 咱们须要某种转换,这个转换步骤称为 transpiling(转译)。transpiling(转译) 是指采用 ES6 语法,转译为旧浏览器能够理解的行为。

Webpack 不知道如何进行转换可是有 loader(加载器) :将它们视为转译器。

babel-loader 是一个 webpack 的 loader(加载器),用于将 ES6 及以上版本转译至 ES5

要开始使用 loader ,咱们须要安装一堆依赖项,如下已 Babel7 为主,升级建议

若是是用 babel7 来转译,须要安装 @babel/core@babel/preset-env@babel/plugin-transform-runtime,而不是 babel-core、babel-preset-env 和 babel-plugin-transform-runtime,它们是用于 babel6 的

使用 @babel/plugin-transform-runtime 的缘由:Babel 使用很是小的助手来完成常见功能。默认状况下,这将添加到须要它的每一个文件中。这种重复有时是没必要要的,尤为是当你的应用程序分布在多个文件上的时候。 transform-runtime 能够重复使用 Babel 注入的程序代码来节省代码,减少体积

使用 @babel/polyfill 的缘由:Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,好比 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(好比 Object.assign)都不会转码。必须使用 @babel/polyfill,为当前环境提供一个垫片。 所谓垫片也就是垫平不一样浏览器或者不一样环境下的差别

(二) 安装依赖并配置

① 安装依赖

npm i @babel/core babel-loader @babel/preset-env @babel/plugin-transform-runtime --save-dev
复制代码

npm i @babel/polyfill @babel/runtime
复制代码
复制代码

② 在项目的根目录中建立名为 .babelrc 的新文件来配置 Babel:

{
  "presets": ["@babel/preset-env"],
  "plugins": ["@babel/plugin-transform-runtime"]
}

复制代码
复制代码

感谢评论区 xcsweb 的指出,若是遇到以下报错

WARNING: We noticed you're using the `useBuiltIns` option without declaring a core-js version. Currently, we assume version 2.x when no version is passed. Since this default version will likely change in future versions of Babel, we recommend explicitly setting the core-js version you are using via the `corejs` option. 
 
You should also be sure that the version you pass to the `corejs` option matches the version specified in your `package.json`'s `dependencies` section. If it doesn't, you need to run one of the following commands: 
 
  npm install --save core-js@2    npm install --save core-js@3 
  yarn add core-js@2              yarn add core-js@3
复制代码
复制代码

不只仅要安装 npm install --save core-js@3 还须要设置 .babelrc 设置 "corejs": 3

{
 "presets": [
   [
     "@babel/preset-env",
     {
       "useBuiltIns": "usage",
       "corejs": 3
     }
   ]
 ],
 "plugins": ["@babel/plugin-transform-runtime"]
}

复制代码
复制代码

③ webpack 配置 loader(加载器)

module: {
  rules: [
    {
      test: /\.js$/, // 使用正则来匹配 js 文件
      exclude: /node_modules/, // 排除依赖包文件夹
      use: {
        loader: 'babel-loader' // 使用 babel-loader
      }
    }
  ]
}
复制代码
复制代码

webpack.config.js 最终配置:

④ 在 app.js 全局引入 @babel/polyfill 并写入 ES6 语法,并执行 npm run build 打包

// 全局引入
import '@babel/polyfill'

// 测试 ES6 语法是否经过 babel 转译
const array = [1, 2, 3]
const isES6 = () => console.log(...array)

isES6()

const arr = [new Promise(() => {}), new Promise(() => {})]

arr.map(item => {
  console.log(item)
})
复制代码
复制代码

⑤ 打包完以后使用 IE 浏览器打开 index.html 文件,看控制台是否有输出,若是是新版的 chrome,是可使用 es6 语法的,因此要用 IE 这个万恶之源试试

全局引入 @babel/polyfill 的这种方式可能会导入代码中不须要的 polyfill,从而使打包体积更大

更改 .babelrc,只转译咱们使用到的

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage"
      }
    ]
  ],
  "plugins": ["@babel/plugin-transform-runtime"]
}
复制代码
复制代码

同时,将全局引入这段代码注释掉,再次打包

// 全局引入
// import '@babel/polyfill'
复制代码
复制代码

体积就减少了不少,可是更多的状况是咱们并不确切的知道项目中引起兼容问题的具体缘由,因此仍是全局引入比较好

(三) 了解 .browserslistrc 配置文件

browserslistrc 用于在不一样前端工具之间共享目标浏览器和 Node.js 版本的配置

能够看看 browserslist 兼容浏览器的页面

当您将如下内容添加到 package.json 时,全部工具都会自动找到目标浏览器:

"browserslist": [
  "> 1%",
  "last 2 version",
  "not ie <= 8"
]
复制代码
复制代码

也能够建立 .browserslistrc 文件单独写配置

# 所支持的浏览器版本

> 1% # 全球使用状况统计选择的浏览器版本

last 2 version # 每一个浏览器的最后两个版本

not ie <= 8 # 排除小于 ie8 如下的浏览器
复制代码
复制代码

该项目仍是使用单首创建配置文件的方式,便于理解,若是以为配置文件很差,也能够写在 package.json

参考文章

webpack4 系列教程 (二): 编译 ES6

babel 7 的使用的我的理解

babel 7 升级建议

browserslist

5.Code Splitting

demo5 源码地址

package.json 文件所用依赖,npm install 安装:

{
  "scripts": {
    "dev": "webpack --mode development",
    "build": "webpack --mode production"
  },
  "devDependencies": {
    "clean-webpack-plugin": "^2.0.0",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.2.3"
  },
  "dependencies": {
    "lodash": "^4.17.11"
  }
}
复制代码
复制代码

咱们在 src/ 文件夹下建立 index.js 文件

import _ from 'lodash'

console.log(_.join(['a', 'b', 'c']))
复制代码
复制代码

目录结构为:

配置 webpack.config.js 文件

const path = require('path')

const CleanWebpackPlugin = require('clean-webpack-plugin')

module.exports = {
  entry: {
    main: './src/index.js'
  },
  output: {
    publicPath: __dirname + '/dist/', // js 引用的路径或者 CDN 地址
    path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录
    filename: '[name].bundle.js', // 代码打包后的文件名
    chunkFilename: '[name].js' // 代码拆分后的文件名
  },
  plugins: [new CleanWebpackPlugin()]
}
复制代码
复制代码

运行 npm run build 打包

在 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>代码分割</title>
  </head>

  <body>
    <script src="./dist/main.bundle.js"></script>
  </body>
</html>
复制代码
复制代码

使用浏览器打开 index.html 文件,进入控制台,能够看到以下信息:a,b,c

若是咱们再改动业务代码,将 index.js 中的代码改成

import _ from 'lodash'

console.log(_.join(['a', 'b', 'c'], '***'))
复制代码
复制代码

再打包,刷新页面能够看到 a***b*****c**

咱们引用的第三方框架和咱们的业务代码一块儿被打包,这样会有一个什么问题?

假设 lodash 为 1M,业务代码也为 1M,打包后假设为 2M

浏览器每次打开页面,都要先加载 2M 的文件,才能显示业务逻辑,这样会使得加载时间变长,

业务代码更新会比较频繁,第三方代码基本不会更改,这样从新打包后,假设为 2M,用户从新打开网页后,又会再加载 2M 文件

浏览器是有缓存的,若是文件没变更的话,就不用再去发送 http 请求,而是直接从缓存中取,这样在刷新页面或者第二次进入的时候能够加快网页加载的速度。

怎么解决呢,能够利用 webpack 中的代码分割

在 webpack4 以前是使用 commonsChunkPlugin 来拆分公共代码,v4 以后被废弃,并使用 splitChunksPlugins

在使用 splitChunksPlugins 以前,首先要知道 splitChunksPlugins 是 webpack 主模块中的一个细分模块,无需 npm 引入

如今咱们来配置 webpack.config.js 文件

const path = require('path')

const CleanWebpackPlugin = require('clean-webpack-plugin')

module.exports = {
  entry: {
    main: './src/index.js'
  },
  output: {
    publicPath: __dirname + '/dist/', // js 引用的路径或者 CDN 地址
    path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录
    filename: '[name].bundle.js', // 代码打包后的文件名
    chunkFilename: '[name].js' // 代码拆分后的文件名
  },
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
  plugins: [new CleanWebpackPlugin()]
}
复制代码
复制代码

上面高亮的代码段就是告诉 webpack,要作代码分割了,这里的 chunks: 'all' 是分割全部代码,包括同步代码和异步代码,webpack 默认是 chunks: 'async' 分割异步代码

咱们使用 npm run dev 来打包开发环境下的代码,这样代码就不会压缩,方便咱们来观察,能够看到代码被分割成两个文件了

打开 dist/main.bundle.js 文件,在最底部能够看到 src/index.js 文件,里面放的是业务逻辑的代码,可是并无 lodash 的代码

打开 dist/vendors~main.js 文件,在最上面能够看到 lodash 模块

再次打开页面,控制台也输出了内容,这样就实现了 Code Splitting(代码分割)

其实没有 webpack 的时候,也是有代码分割的,不过是须要咱们本身手动的分割,而如今使用了 webpack,经过这种配置项的方式,它会自动帮咱们去作代码分割

仔细看分割完的代码名称,vendors~main.js,咱们对分割完的名称进行更改

仍是在 splitChunks 的配置项中,添加 cacheGroups 对象

optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      vendors: {
        name: 'vendors'
      }
    }
  }
}
复制代码
复制代码

再次打包就能够看到效果了,cacheGroups 的默认配置会定义 vendorsdefault

test: /[\\/]node_modules[\\/]/, 使用正则过滤,只有 node_modules 引入的第三方库会被分割

为了验证默认配置,咱们将 splitChunks 属性设置为空对象,再次打包

打包完发现只有一个文件,这是为何?

由于 chunks 默认为 async,只会分割异步的代码,而以前咱们写的都是同步的代码,先 import lodash,再去写业务逻辑,如今使用异步的方式来作,将 index.js 中的代码改成如下:

function getComponent() {
  // 使用 异步的形式导入 lodash,default: _ 表示用 _ 代指 lodash
  return import('lodash').then(({ default: _ }) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['hello', 'world'], '-')
    return element
  })
}

getComponent().then(element => {
  document.body.appendChild(element)
})
复制代码
复制代码

这里分割出了 0.jsmain.bundle.js,0 是以 id 为编号来命名

因此通常咱们设置 chunks 为 all,异步、同步代码都打包

如今咱们将 webpack 官网上的默认配置拷贝到咱们的 webpack.config.js 中来分析一下

optimization: {
  splitChunks: {
    chunks: 'async',
    minSize: 30000,
    maxSize: 0,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    automaticNameDelimiter: '~',
    name: true,
    cacheGroups: {
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        priority: -10
      },
      default: {
        minChunks: 2,
        priority: -20,
        reuseExistingChunk: true
      }
    }
  }
}
复制代码
复制代码

webpack 代码分割的配置是这样的,好比咱们要分割 jQuery 和 lodash 这样的第三方库,它会先通过 chunks、minSize、maxSize、minChunks 等等,知足条件后生成 jQuery 和 lodash 两个文件,而后放入 cacheGroup 中缓存着,再根据你在 cacheGroup 中配置的来决定是将两个文件整合到一个文件打包,仍是单独分开打包,好比上面代码中的 vendors,就是将 node_modules 中全部的第三方库都打包到 vendors.js 文件中,若是你还想继续分割能够这么作

cacheGroups: {
  lodash: {
    name: 'lodash',
    test: /[\\/]node_modules[\\/]lodash[\\/]/,
    priority: 5  // 优先级要大于 vendors 否则会被打包进 vendors
  },
  vendors: {
    test: /[\\/]node_modules[\\/]/,
    priority: -10
  },
  default: {
    minChunks: 2,
    priority: -20,
    reuseExistingChunk: true
  }
}
复制代码
复制代码

感谢评论区 anlinsir 的指出,若是打包有报错 Support for the experimental syntax ‘dynamicImport’ isn't currently enabled,这是由于 dynamicImport 仍是实验性的语法,webpack 不支持,须要安装插件来支持,具体步骤能够参考: www.cnblogs.com/chaoyueqi/p…

再次打包,就能够看到 lodash 被分割出来了,之后使用第三方库均可以用这种配置来单独分割成一个 js 文件,好比 element-ui注意设置 priority 的值很重要,优先级越高的会越先被打包

若是 index.js 引入了 A.js 和 B.js,同时 A、B 又引入了 common,common 被引入了两次,能够被称为公共模块

目录结构为:

代码以下:

// a,js
import './common'
console.log('A')
export default 'A'

// b.js
import './common'
console.log('B')
export default 'B'

// common.js
console.log('公共模块')
export default 'common'

// index.js
// 异步代码
import(/* webpackChunkName: 'a'*/ './a').then(function(a) {
  console.log(a)
})

import(/* webpackChunkName: 'b'*/ './b').then(function(b) {
  console.log(b)
})

// 异步代码
function getComponent() {
  // 使用异步的形式导入 lodash,default: _ 表示用 _ 代指 lodash
  return import('lodash').then(({ default: _ }) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['hello', 'world'], '-')
    return element
  })
}

getComponent().then(element => {
  document.body.appendChild(element)
})
复制代码
复制代码

上面那种异步的写法可能比较绕,如今精简一下,而且 webpack 对异步代码经过注释能够直接修改打包后的名称,如下代码所有以异步的形式引入

// 异步代码
import(/* webpackChunkName: 'a'*/ './a').then(function(a) {
  console.log(a)
})

import(/* webpackChunkName: 'b'*/ './b').then(function(b) {
  console.log(b)
})

import(/* webpackChunkName: 'use-lodash'*/ 'lodash').then(function(_) {
  console.log(_.join(['1', '2']))
})
复制代码
复制代码

将 minChunks 设置为 2,最小公用 2 次才分割

optimization: {
  splitChunks: {
    chunks: 'all',
    minSize: 30000,
    maxSize: 0,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    automaticNameDelimiter: '~',
    name: true,
    cacheGroups: {
      lodash: {
        name: 'lodash',
        test: /[\\/]node_modules[\\/]lodash[\\/]/,
        priority: 10
      },
      commons: {
        name: 'commons',
        minSize: 0, //表示在压缩前的最小模块大小,默认值是 30kb
        minChunks: 2, // 最小公用次数
        priority: 5, // 优先级
        reuseExistingChunk: true // 公共模块必开启
      },
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        priority: -10
      },
      default: {
        minChunks: 2,
        priority: -20,
        reuseExistingChunk: true
      }
    }
  }
}
复制代码
复制代码

这里分割出了 lodash 和咱们在注释中定义的 use-lodash,前者是第三库,后者是使用第三库写的业务代码,也能被分割出来

这里之因此会自动引入分割后的依赖,能够查看打包后的 main.bundle.js 文件

甚至咱们能够打断点来看它是怎么运行的,如下为 gif 动图演示

经常使用的配置项在下面的表格中,更多配置详情见官网

参考文章

webpack4 系列教程 (三): 多页面解决方案 -- 提取公共代码

webpack 官网

6.Lazy Loading、Prefetching

demo6 源码地址

在 demo5 的基础上修改 index.js 文件,并删除了多余的 js 文件

document.addEventListener('click', function() {
  import(/* webpackChunkName: 'use-lodash'*/ 'lodash').then(function(_) {
    console.log(_.join(['3', '4']))
  })
})
复制代码
复制代码

这段代码表示的是,当点击页面的时候,异步加载 lodash 并输出内容,打包后打开 index.html 文件,演示以下:

第一次进入页面的时候,并无加载 lodash 和 use-lodash,当我点击网页的时候,浏览器再去加载,而且控制台输出内容,这就是代码懒加载,若是有用过 vue-router 的朋友应该会知道路由懒加载,而且官方也推荐使用懒加载的写法,就是为告终合 webpack,下图是 vue-cli3 生成的项目

其实懒加载就是经过 import 去异步的加载一个模块,具体何时加载,这个要根据业务来写,好比弹窗组件,模态框组件等等,都是点击按钮后再出现。

懒加载能加快网页的加载速度,若是你把详情页、弹窗等页面所有打包到一个 js 文件中,用户若是只是访问首页,只须要首页的代码,不须要其余页面的代码,加入多余的代码只会使加载时间变长,因此咱们能够对路由进行懒加载,只有当用户访问到对应路由的时候,再去加载对应模块

懒加载并非 webpack 里的概念,而是 ES6 中的 import 语法,webpack 只是可以识别 import 语法,能进行代码分割而已。

import 后面返回的是一个 then,说明这是一个 promise 类型,一些低端的浏览器不支持 promise,好比 IE ,若是要使用这种异步的代码,就要使用 babel 以及 babel-polyfill 来作转换,由于我使用的是 73 版本的 chrome 浏览器,对 ES6 语法是支持的,因此我并无安装 babel 也能使用

更改 index.js 文件

document.addEventListener('click', function() {
  const element = document.createElement('div')
  element.innerHTML = 'Hello World'
  document.body.appendChild(element)
})
复制代码
复制代码

从新打包,并打开 index.html ,打开浏览器控制台,按 ctrl + shift + p ,输入 coverage

就能看到当前页面加载的 js 代码未使用率,红色区域表示未被使用的代码段

演示:

打开 coverage 若是没出现分析的文件,记得刷新一下

这里一开始红色区域的代码未被使用,当我点击了页面后,变成绿色,页面出现了 Hello World,说明代码被使用了

页面刚加载的时候,异步的代码根本就不会执行,可是咱们却把它下载下来,实际上就会浪费页面执行性能,webpack 就但愿像这样交互的功能,应该把它放到一个异步加载的模块去写

新建一个 click.js 文件

function handleClick() {
  const element = document.createElement('div')
  element.innerHTML = 'Dell Lee'
  document.body.appendChild(element)
}

export default handleClick
复制代码
复制代码

而且将 index.js 文件改成异步的加载模块:

document.addEventListener('click', () => {
  import('./click.js').then(({ default: func }) => {
    func()
  })
})
复制代码
复制代码

从新打包,使用 coverage 分析

演示:

当加载页面的时候,没有加载业务逻辑,当点击网页的时候,才会加载 1.js 模块,也就是咱们在 index.js 中异步引入的 click.js

这么去写代码,才是使页面加载最快的一种方式,写高性能前端代码的时候,不关是考虑缓存,还要考虑代码使用率

因此 webpack 在打包过程当中,是但愿咱们多写这种异步的代码,才能提高网站的性能,这也是为何 webpack 的 splitChunks 中的 chunks 默认是 async,异步的

异步能提升你网页打开的性能,而同步代码是增长一个缓存,对性能的提高是很是有限的,由于缓存通常是第二次打开网页或者刷新页面的时候,缓存颇有用,可是网页的性能通常是用户第一次打开网页,看首屏的时候。

固然,这也会出现另外一个问题,就是当用户点击的时候,才去加载业务模块,若是业务模块比较大的时候,用户点击后并无立马看到效果,而是要等待几秒,这样体验上也很差,怎么去解决这种问题

一:若是访问首页的时候,不须要加载详情页的逻辑,等用户首页加载完了之后,页面展现出来了,页面的带宽被释放出来了,网络空闲了,再「偷偷」的去加载详情页的内容,而不是等用户去点击的时候再去加载

这个解决方案就是依赖 webpack 的 Prefetching/Preloading 特性

修改 index.js

document.addEventListener('click', () => {
  import(/* webpackPrefetch: true */ './click.js').then(({ default: func }) => {
    func()
  })
})
复制代码
复制代码

webpackPrefetch: true 会等你主要的 JS 都加载完了以后,网络带宽空闲的时候,它就会预先帮你加载好

从新打包后刷新页面,注意看 Network

当网页打开的时候,main.bundle.js 被加载完了,网络空闲了,就会预先加载 1.js 耗时 14ms,等我去点击页面的时候,Network 又多了一个 1.js,耗时 2ms,这是由于第一次加载完了 1.js,被浏览器给缓存起来了,等我点击的时候,浏览器直接从缓存中取,响应速度很是快

这里咱们使用的是 webpackPrefetch,还有一种是 webpackPreload

与 prefetch 相比,Preload 指令有不少不一样之处

Prefetch 会等待核心代码加载完以后,有空闲以后再去加载。Preload 会和核心的代码并行加载,仍是不推荐

针对优化,不只仅是局限于缓存,缓存能带来的代码性能提高是很是有限的,而是如何让代码的使用率最高,有一些交互后才用的代码,能够写到异步组件里面去,经过懒加载的形式,去把代码逻辑加载进来,这样会使得页面访问速度变的更快,若是你以为懒加载会影响用户体验,可使用 Prefetch 这种方式来预加载,不过在某些游览器不兼容,会有兼容性的问题,重点不是在 Prefetch 怎么去用,而是在作前端代码性能优化的时候,缓存不是最重要的点,最重要的是代码使用的覆盖率上(coverage)

7.自动生成 HTML 文件

demo7 源码地址

通过上面几个小节的操做,有没有以为每次要去更改 index.html 中引入 js 文件很麻烦,一旦打包的名字变动后,也要对应的去修改 index.html 引入的 js 名称,这个时候就要使用一个插件来帮助咱们,打包完以后自动生成 HTML 文件并自动引入打包后的 js 文件

(一) 安装依赖

npm i html-webpack-plugin html-loader --save-dev
复制代码
复制代码

package.json 以下:

{
  "scripts": {
    "dev": "webpack --mode development",
    "build": "webpack --mode production"
  },
  "devDependencies": {
    "clean-webpack-plugin": "^2.0.0",
    "html-loader": "^0.5.5",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.2.3"
  },
  "dependencies": {
    "lodash": "^4.17.11"
  }
}
复制代码
复制代码

(二) 更改配置文件

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      // 打包输出HTML
      title: '自动生成 HTML',
      minify: {
        // 压缩 HTML 文件
        removeComments: true, // 移除 HTML 中的注释
        collapseWhitespace: true, // 删除空白符与换行符
        minifyCSS: true // 压缩内联 css
      },
      filename: 'index.html', // 生成后的文件名
      template: 'index.html' // 根据此模版生成 HTML 文件
    })
  ]
}
复制代码
复制代码

HtmlWebpackPlugin 是在 plugin 这个选项中配置的。经常使用参数含义以下:

  • title: 打包后生成 html 的 title
  • filename:打包后的 html 文件名称
  • template:模板文件(例子源码中根目录下的 index.html)
  • chunks:和 entry 配置中相匹配,支持多页面、多入口
  • minify:压缩选项

因为使用了 title 选项,则须要在 template 选项对应的 html 的 title 中加入 <%= htmlWebpackPlugin.options.title %>

const path = require('path')

const CleanWebpackPlugin = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin') // 引入插件

module.exports = {
  entry: {
    page: './src/page.js'
  },
  output: {
    publicPath: __dirname + '/dist/', // js 引用的路径或者 CDN 地址
    path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录
    filename: '[name].bundle.js', // 代码打包后的文件名
    chunkFilename: '[name].js' // 代码拆分后的文件名
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      // 打包输出HTML
      title: '自动生成 HTML',
      minify: {
        // 压缩 HTML 文件
        removeComments: true, // 移除 HTML 中的注释
        collapseWhitespace: true, // 删除空白符与换行符
        minifyCSS: true // 压缩内联 css
      },
      filename: 'index.html', // 生成后的文件名
      template: 'index.html' // 根据此模版生成 HTML 文件
    })
  ],
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        lodash: {
          name: 'chunk-lodash', // 单独将 lodash 拆包
          priority: 10, // 优先级要大于 commons 否则会被打包进 commons
          test: /[\\/]node_modules[\\/]lodash[\\/]/
        },
        commons: {
          name: 'chunk-commons',
          minSize: 1, //表示在压缩前的最小模块大小,默认值是 30kb
          minChunks: 2, // 最小公用次数
          priority: 5, // 优先级
          reuseExistingChunk: true // 公共模块必开启
        }
      }
    }
  }
}
复制代码
复制代码

(三) 打包并测试

运行 npm run build

打开 dist 文件夹里自动生成的 index.html

在浏览器中打开 index.html 文件,打开控制台也发现有输出,OK,自动生成 HTML 文件成功

细心的朋友可能会发现一个问题,生成后的 HTML 文件引入的 JS 为绝对路径,可是真实的项目打完包以后都是部署在服务器上,用绝对路径确定不行,由于你本地电脑的绝对路径放在服务器上确定找不到

咱们要将引入的 js 文件从绝对路径改成相对路径

修改 webpack.config.js 文件

找到 output 输出配置,更改 publicPath 公共路径,修改成 ./ 绝对路径

output: {
    publicPath: './', // js 引用的路径或者 CDN 地址
    path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录
    filename: '[name].bundle.js', // 代码打包后的文件名
    chunkFilename: '[name].js' // 代码拆分后的文件名
  },
复制代码
复制代码

再次打包,看打包后的 index.html 文件,打开浏览器测试,也是没问题的

8.处理 CSS/SCSS 文件

demo8 源码地址

(一) 准备工做

CSS 在 HTML 中的经常使用引入方法有 <link> 标签和 <style> 标签两种,因此此次就是结合 webpack 特色实现如下功能:

  • 将 css 经过 link 标签引入
  • 将 css 放在 style 标签里

下图展现了此次的目录代码结构:

此次咱们须要用到 css-loaderstyle-loader 等 loader,跟 babel 同样,webpack 不知道将 CSS 提取到文件中。须要使用 loader 来加载对应的文件

css-loader:负责解析 CSS 代码,主要是为了处理 CSS 中的依赖,例如 @import 和 url() 等引用外部文件的声明

style-loader 会将 css-loader 解析的结果转变成 JS 代码,运行时动态插入 style 标签来让 CSS 代码生效。

(二) 安装依赖

npm i css-loader style-loader --save-dev
复制代码
复制代码

package.json 以下:

{
  "scripts": {
    "dev": "webpack --mode development",
    "build": "webpack --mode production"
  },
  "devDependencies": {
    "clean-webpack-plugin": "^2.0.0",
    "css-loader": "^2.1.0",
    "html-loader": "^0.5.5",
    "html-webpack-plugin": "^3.2.0",
    "mini-css-extract-plugin": "^0.5.0",
    "style-loader": "^0.23.1",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.2.3"
  }
}
复制代码
复制代码

更改配置文件

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/, // 针对 .css 后缀的文件设置 loader
        use: ['style-loader', 'css-loader']
      }
    ]
  }
}
复制代码
复制代码

配置 module 中的 rules 属性,和配置 babel 同样,首先在 test 中使用正则来过滤 .css 文件,对 .css 文件使用 loader,'style-loader', 'css-loader'

在 base.css 中写入样式

*,
body {
  margin: 0;
  padding: 0;
}
html {
  background: red;
}
复制代码
复制代码

并在 app.js 中引入 base.css

import style from './css/base.css'
复制代码
复制代码

配置文件完整代码:

const path = require('path')

const CleanWebpackPlugin = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin') // 引入插件

module.exports = {
  entry: {
    app: './src/app.js'
  },
  output: {
    publicPath: './', // js 引用的路径或者 CDN 地址
    path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录
    filename: '[name].bundle.js', // 代码打包后的文件名
    chunkFilename: '[name].js' // 代码拆分后的文件名
  },
  module: {
    rules: [
      {
        test: /\.css$/, // 针对 .css 后缀的文件设置 loader
        use: ['style-loader', 'css-loader'] // 使用 loader
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      // 打包输出HTML
      title: '自动生成 HTML',
      minify: {
        // 压缩 HTML 文件
        removeComments: true, // 移除 HTML 中的注释
        collapseWhitespace: true, // 删除空白符与换行符
        minifyCSS: true // 压缩内联 css
      },
      filename: 'index.html', // 生成后的文件名
      template: 'index.html', // 根据此模版生成 HTML 文件
      chunks: ['app'] // entry中的 app 入口才会被打包
    })
  ]
}
复制代码
复制代码

项目打包,查看 dist 文件夹

发现并没有生成 CSS 文件,可是打开 index.html 是有样式的

缘由是:style-loader, css-loader 两个 loader 的处理后,CSS 代码会转变为 JS,和 index.js 一块儿打包

能够发现是经过 <style> 标签注入的 css

若是须要单独把 CSS 文件分离出来,咱们须要使用 mini-css-extract-plugin 插件。

以前是使用 extract-text-webpack-plugin 插件,此插件与 webpack4 不太匹配,如今使用 mini-css-extract-plugin

确保将 webpack 更新到 4.2.0 版及以上。不然 mini-css-extract-plugin 将无效!

目前还不支持热更新,也就是在开发环境下更改了 css,须要手动的刷新页面才会看到效果,目前这个插件通常在生产环境中使用,开发环境下仍是使用 'style-loader',具体见官网配置

npm i mini-css-extract-plugin --save-dev
复制代码
复制代码

更改配置文件:

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/, // 针对 .css 后缀的文件设置 loader
        use: [
          {
            loader: MiniCssExtractPlugin.loader
          },
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css'
    })
  ]
}
复制代码
复制代码

完整代码:

const path = require('path')

const CleanWebpackPlugin = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const MiniCssExtractPlugin = require('mini-css-extract-plugin') // 将 css 单独打包成文件

module.exports = {
  entry: {
    app: './src/app.js'
  },
  output: {
    publicPath: './', // js 引用的路径或者 CDN 地址
    path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录
    filename: '[name].bundle.js', // 代码打包后的文件名
    chunkFilename: '[name].js' // 代码拆分后的文件名
  },
  module: {
    rules: [
      {
        test: /\.css$/, // 针对 .css 后缀的文件设置 loader
        use: [
          {
            loader: MiniCssExtractPlugin.loader
          },
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      // 打包输出HTML
      title: '自动生成 HTML',
      minify: {
        // 压缩 HTML 文件
        removeComments: true, // 移除 HTML 中的注释
        collapseWhitespace: true, // 删除空白符与换行符
        minifyCSS: true // 压缩内联 css
      },
      filename: 'index.html', // 生成后的文件名
      template: 'index.html', // 根据此模版生成 HTML 文件
      chunks: ['app'] // entry中的 app 入口才会被打包
    }),
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css'
    })
  ]
}
复制代码
复制代码

这样只是生成了单独的 css 文件,可是并无压缩,引入 optimize-css-assets-webpack-plugin 插件来实现 css 压缩

npm install optimize-css-assets-webpack-plugin --save-dev
复制代码
复制代码

完整代码:

const path = require('path')

const CleanWebpackPlugin = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const MiniCssExtractPlugin = require('mini-css-extract-plugin') // 将 css 单独打包成文件
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin') // 压缩 css

module.exports = {
  entry: {
    app: './src/app.js'
  },
  output: {
    publicPath: './', // js 引用的路径或者 CDN 地址
    path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录
    filename: '[name].bundle.js', // 代码打包后的文件名
    chunkFilename: '[name].js' // 代码拆分后的文件名
  },
  module: {
    rules: [
      {
        test: /\.css$/, // 针对 .css 后缀的文件设置 loader
        use: [
          {
            loader: MiniCssExtractPlugin.loader
          },
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      // 打包输出HTML
      title: '自动生成 HTML',
      minify: {
        // 压缩 HTML 文件
        removeComments: true, // 移除 HTML 中的注释
        collapseWhitespace: true, // 删除空白符与换行符
        minifyCSS: true // 压缩内联 css
      },
      filename: 'index.html', // 生成后的文件名
      template: 'index.html', // 根据此模版生成 HTML 文件
      chunks: ['app'] // entry中的 app 入口才会被打包
    }),
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css'
    }),
    new OptimizeCssAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require('cssnano'), //用于优化\最小化 CSS 的 CSS处理器,默认为 cssnano
      cssProcessorOptions: { safe: true, discardComments: { removeAll: true } }, //传递给 cssProcessor 的选项,默认为{}
      canPrint: true //布尔值,指示插件是否能够将消息打印到控制台,默认为 true
    })
  ]
}
复制代码
复制代码

再打开 css 文件能够发现已经被压缩了,而且打开 index.html 也是有样式的

(三) 处理 SCSS 文件

安装 sass 依赖:

npm i node-sass sass-loader --save-dev
复制代码
复制代码

在 src 文件夹下新增 scss 文件夹及 main.scss 文件

main.scss 引入样式

$bgColor: black !default;
*,
body {
  margin: 0;
  padding: 0;
}
html {
  background-color: $bgColor;
}
复制代码
复制代码

在 app.js 中引入 main.scss 文件

import './css/base.css'
import './scss/main.scss'
复制代码
复制代码

修改配置文件

const path = require('path')

const CleanWebpackPlugin = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const MiniCssExtractPlugin = require('mini-css-extract-plugin') // 将 css 单独打包成文件
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin') // 压缩 css

module.exports = {
  entry: {
    app: './src/app.js'
  },
  output: {
    publicPath: './', // js 引用的路径或者 CDN 地址
    path: path.resolve(__dirname, 'dist'), // 打包文件的输出目录
    filename: '[name].bundle.js', // 代码打包后的文件名
    chunkFilename: '[name].js' // 代码拆分后的文件名
  },
  module: {
    rules: [
      {
        test: /\.(scss|css)$/, // 针对 .scss 或者 .css 后缀的文件设置 loader
        use: [
          {
            loader: MiniCssExtractPlugin.loader
          },
          'css-loader',
          'sass-loader' // 使用 sass-loader 将 scss 转为 css
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      // 打包输出HTML
      title: '自动生成 HTML',
      minify: {
        // 压缩 HTML 文件
        removeComments: true, // 移除 HTML 中的注释
        collapseWhitespace: true, // 删除空白符与换行符
        minifyCSS: true // 压缩内联 css
      },
      filename: 'index.html', // 生成后的文件名
      template: 'index.html', // 根据此模版生成 HTML 文件
      chunks: ['app'] // entry中的 app 入口才会被打包
    }),
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css'
    }),
    new OptimizeCssAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require('cssnano'), //用于优化\最小化 CSS 的 CSS处理器,默认为 cssnano
      cssProcessorOptions: { safe: true, discardComments: { removeAll: true } }, //传递给 cssProcessor 的选项,默认为{}
      canPrint: true //布尔值,指示插件是否能够将消息打印到控制台,默认为 true
    })
  ]
}
复制代码
复制代码

module.rules.use 数组中,loader 的位置。根据 webpack 规则:放在最后的 loader 首先被执行,从上往下写的话是下面先执行,从左往右写的话是右边先执行

['style-loader', 'css-loader', 'sass-loader']
复制代码
复制代码

执行顺序为 sass-loader --> css-loader --> style-loader

首先应该利用 sass-loader 将 scss 编译为 css,剩下的配置和处理 css 文件相同。

打包后再打开 index.html 文件会发现样式已经被 main.scss 中写的覆盖了,处理 scss 成功

(四) 为 CSS 加上浏览器前缀

安装 postcss-loaderautoprefixer 依赖

npm install postcss-loader autoprefixer --save-dev
复制代码
复制代码

src/scss/main.css 中添加这段代码

.example {
  display: grid;
  transition: all 0.5s;
  user-select: none;
  background: linear-gradient(to bottom, white, black);
}
复制代码
复制代码

有两种方式来配置 postcss,第一种是直接写在 webpack.config.js 中

module: {
  rules: [
    {
      test: /\.(sa|sc|c)ss$/, // 针对 .sass .scss 或者 .css 后缀的文件设置 loader
      use: [
        {
          loader: MiniCssExtractPlugin.loader
        },
        'css-loader',
        // 使用 postcss 为 css 加上浏览器前缀
        {
          loader: 'postcss-loader',
          options: {
            plugins: [require('autoprefixer')]
          }
        },
        'sass-loader' // 使用 sass-loader 将 scss 转为 css
      ]
    }
  ]
}
复制代码
复制代码

打包完以后,查看 dist/app.css 文件

第二种方式,在 webpack.config.js 同级目录下,新建 postcss.config.js 配置文件

module.exports = {
  plugins: [require('autoprefixer')]
}
复制代码
复制代码

同时在 webpack.config.js 中

module: {
  rules: [
    {
      test: /\.(sa|sc|c)ss$/, // 针对 .sass .scss 或者 .css 后缀的文件设置 loader
      use: [
        {
          loader: MiniCssExtractPlugin.loader
        },
        'css-loader',
        'postcss-loader', // 使用 postcss 为 css 加上浏览器前缀
        'sass-loader' // 使用 sass-loader 将 scss 转为 css
      ]
    }
  ]
}
复制代码
复制代码

因为 module 中的 rules 是倒着执行的,以上的执行顺序是 sass-loader -> postcss-loader -> css-loader -> MiniCssExtractPlugin.loader

postcss-loader 在 css-loader 和 style-loader 以后使用,但要在其余预处理器加载器以前,例如 sass | less | stylus-loader

补充:

在 css-loader 中使用 importLoaders 属性

module: {
  rules: [
    {
      test: /\.(sa|sc|c)ss$/, // 针对 .sass .scss 或者 .css 后缀的文件设置 loader
      use: [
        {
          loader: MiniCssExtractPlugin.loader
        },
        {
          loader: css - loader,
          options: {
            importLoaders: 2
          }
        },
        'postcss-loader', // 使用 postcss 为 css 加上浏览器前缀
        'sass-loader' // 使用 sass-loader 将 scss 转为 css
      ]
    }
  ]
}
复制代码
复制代码

importLoaders: 2 表示:在一个 css 中引入了另外一个 css,也会执行以前两个 loader,即 postcss-loader 和 sass-loader

参考:webpack 官网指南

相关文章
相关标签/搜索