《深入浅出Webpack》学习笔记

入门

先简单地提下模块化的思想。

模块化

简单来说就是将复杂的系统分解成各个简单的子模块,便于开发和维护。

一般 JavaScript 模块化规范有 CommonJS,AMD 和 ES6 中的模块化。

CommonJS

其核心思想就是利用 require 来同步加载依赖的模块,通过 module.exports 来暴露模块的接口。

优点在于

  • 代码可在 node.js 环境下运行
  • NPM 包中大部分模块都支持 CommonJS

缺点在于:无法在浏览器环境下运行,要想运行必须通过工具转换。

AMD

异步模块定义(Asynchronous Module Definition) 是以异步的方式加载模块,主要用于解决浏览器的模块化加载,比如像 require.js.

优点

  • 可无需转换即可在浏览器环境中运行
  • 可异步加载模块
  • 可并行加载多个模块
  • 可在node和浏览器环境中运行

缺点:浏览器没有原生支持 AMD,若要使用需要导入相应的包。

ES6 的模块化

它是 ECMA 提出的 JavaScript 模块化规范,是在语言层面上实现的。他将逐渐取代 CommonJS 和 AMD 规范。但是目前仍然无法运行在大部分 JavaScript 运行环境中,所以需要转换工具转成 ES5 后才能运行。

tip: CSS 样式文件中也开始支持模块化,比如 SASS, SCSS, less

不仅这些模块化规范需要转换工具才能运行,还有目前流行 React 中 JSX 语法和 vue 中的 template 也需要进行转换,还有当前流行 es6,typescript 也需要转换。所以随着项目越来越复杂和技术越来越新,不可避免的需要出现构建工具来统一的管理这些转换工具。

常见的构建工具

构建工具主要的工作如下:

  • 代码转换
  • 文件优化:压缩静态资源文件
  • 分割代码,将公共代码分离出来
  • 模块合并
  • 实现热更新
  • 自动校验
  • 实现自动发布

主要的构建工具有(诞生的时间排序):

  • Npm Script:属于 NPM 内置的功能,在 package.json 文件中的 script 对象中配置,其中每个属性对应一个 shell 命令。
  • Grunt:通过执行任务的方式进行构建。
  • Gulp:一种基于流的自动化构建工具,除了可以管理和执行任务,还能监听文件,读写文件。
  • Webpack:专注于构建模块化项目,在 webpack 里,一切文件都被视为模块。通过 loader 转换文件,通过 Plugin 注入钩子,最后输出有多个模块组合而成的文件。
  • Rollup:和 webpack 类似,他的亮点就是可以 Tree shaking,以减小输出文件大小和提高运行性能。后期 webpack 也是实现了 Tree Shaking。

选择 Webpack 的原因

  • 可以为新项目提供一站式的解决方案
  • 有良好的生态和维护团队
  • 被大量使用,经受住了大家的考验。

安装

# 初始化 npm 包
npm init

# 以下三选一
# 稳定版
npm install webpack -D
# 指定版
npm install webpack@<version> -D
# 最新的体验版
npm install webpack@beta -D

请看以下 webpack 使用实例(文件都在根目录)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div id="app"></div>
<!-- 导入webpack打包输出的文件 -->
<script src="./dist/bundle.js"></script>
</body>
</html>

show.js

function show(content){
  document.getElementById("app").innerText = "hello, " + content;
}

// 通过 CommonJS 规范导出show函数
module.exports=show;

main.js

// 通过 CommonJS 规范导入show函数
const show = require('./show.js')

show("webpack")

wepack.config.js

const path = require('path')

module.exports = {
  // javascript 执行入口文件
  entry: './main.js',
  output: {
    // 将所有依赖的模块合并输出到bundle.js文件中
    filename: "bundle.js",
    // 将输出文件都放到此目录下
    path: path.resolve(__dirname, "./dist")
  }
}

然后在根目录执行打包命令 webpack

会看到生成 dist 目录,里面会有打包好的文件bundle.js。

打开 index.html 文件便可看到构建效果。

从 Webpack 2版本开始,就已经内置了转换 CommonJS、ES6、AMD模块化的功能

简单的 Loader 使用示例

假如我们需要给页面添加样式,新建了 main.css 文件:

#app{
    color:red;
}

为了让样式生效,我们还需要在 main.js 中导入:

// 通过 CommonJS 规范导入 main.css
require('main.css')
// 通过 CommonJS 规范导入show函数
const show = require('./show.js')

show("webpack")

因为 webpack 原生是不支持非 JavaScript 文件转换的,所以直接通过webpack 打包命令可能会报错:

ERROR in ./main.css 1:0
Module parse failed: Unexpected character '#' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> #app{
|     color: red;
| }
 @ ./main.js 2:0-21

由此需要引出 Loader 概念,它就是用来处理文件转换的。如果需要支持 CSS 模块的转换,首先需要先安装相关 Loader:

npm install -D style-loader css-loader

之后在 webpack.config.js 文件中配置:

const path = require('path')

module.exports = {
  // javascript 执行入口文件
  entry: './main.js',
  output: {
    // 将所有依赖的模块合并输出到bundle.js文件中
    filename: "bundle.js",
    // 将输出文件都放到此目录下
    path: path.resolve(__dirname, "./dest")
  },
  // 配置 Loader
  module: {
    rules: [
      {
        test:/\.css$/,
        // css-loader负责读取css文件,再通过 style-loader 注入到js中
        use:['style-loader', 'css-loader?minimize'] 
      }
    ]
  }
}

通过在 module.rules 数组中配置一系列规则,以便告知 webpack 根据哪些 Loader 去转换指定的文件。其中 use 属性需要注意以下几点:

  • Loader 的执行顺序是由后向前的。

  • 每个 Loader 传参方式有两种:

    • 通过 URL querystring 的方式,比如 css-loader?minimize 就是告知 Loader 要开启 CSS 压缩。

    • 通过options 方式:

      const path = require('path')
      
      module.exports = {
        // javascript 执行入口文件
        entry: './main.js',
        output: {
          // 将所有依赖的模块合并输出到bundle.js文件中
          filename: "bundle.js",
          // 将输出文件都放到此目录下
          path: path.resolve(__dirname, "./dest")
        },
        module: {
          rules: [
            {
              test:/\.css$/,
              use:['style-loader', {
                loader:'css-loader',
                options: {
                  minimize:true
                }
              }]
            }
          ]
        }
      }
      
      

注:webpack3.0 和 css-loader1.0 以上就不支持 minimize 了,为了讲解传参方式,还是用了它。

简单的 Plugin 使用示例

Plugin 是用来扩展 webpack 功能的,通过在构建流程中注入钩子实现。比如我们需要将 CSS 从 bundle.js 文件中分离出来,可以用 extract-text-webpack-plugin ,首先还是先安装 Plugin:

npm install extract-text-webpack-plugin -D

然后在 webpack.config.js 文件中配置:

const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
let plugins = []
plugins.push(new ExtractTextPlugin({
  // 提取出来的 css 文件名
  filename: `[name]_[contenthash:8].css`
}))
module.exports = {
  // javascript 执行入口文件
  entry: './main.js',
  output: {
    // 将所有依赖的模块合并输出到bundle.js文件中
    filename: "bundle.js",
    // 将输出文件都放到此目录下
    path: path.resolve(__dirname, "./dist")
  },
  module: {
    rules: [
      {
        test:/\.css$/,
        use:ExtractTextPlugin.extract({
          fallback:'style-loader',
          use:['css-loader']
        })
      }
    ]
  },
  plugins
}

**注:webpack4.0 以上就不再支持 extract-text-webpack-plugin **,可以通过安装新的版本解决:npm install --D extract-text-webpack-plugin@next

使用 DevServer

一般我们开发网页调试有以下几个需求:

  1. 提供 http 服务而不是不是本地文件预览
  2. 监听文件内容变化,实时更新页面
  3. 提供 sourcemap 功能,以便调试页面

后两个功能 webpack 原生支持。第一个需求可使用 webpack-dev-server,它可以启动 http 服务用于网络请求,并同时执行 webpack 命令,将构建后的文件存储在内存中。所以执行 webpack-dev-server 命令是不会创建 dist 目录,所以在 .index.html 中静态资源路径需要将以前的./dist/bundle.js 修改为 ./bundle.js

第二个功能可以通过给 webpack-dev-server 命令加 --hot 参数实现:

也就是常说的热替换,在不重新刷新页面的情况下通过替换旧模块来更新页面。

webpack-dev-server --hot

第三个功能可以通过给 webpack-dev-server 命令加 --devtool source-map 参数实现:

webpack-dev-server --devtool source-map

核心概念

以下几个核心概念需要清楚:

  • Entry:入口
  • Module:模块
  • Chunk:代码块
  • Loader:模块转换器
  • Plugin:扩展插件
  • Output:输出结果

webpack 的流程:

webpack 启动时会先找到 Entry 配置里 Module,去解析 Entry 依赖的所有 Module。每找到一个 Module 就会根据 Loader 里的相应规则进行转换,转换后会再次解析当前 Module 依赖的所有 Module,这些模块会以 Entry 为单位进行分组,一个 Entry及其依赖的所有 Module 形成一个组,也就是一个 chunk,webpack 会将所有的 chunk 转换成输出文件。在构建中,webpack会在恰当的时机执行 plugin 中的逻辑。

配置

webpack 配置总的来说有两种方式,一种通过配置文件webpack.config.js,另一种则是通过命令行的方式,如webpack --devtool map-resource

本章主要讲解下以下配置概念:

  • Entry:配置模块的入口文件
  • Output:配置构建输出文件信息
  • Module:配置解析模块的规则
  • Resolve:配置寻找模块的规则
  • Plugins:配置扩展插件
  • DevServer:配置 DevServer
  • 配置总结

Entry

context

webpack 在处理相对路径时会以 context 为根目录,context 默认为当前配置文件目录,淡然也可以通过以下两种方式修改:

  1. 修改配置文件webpack.config.js
module.exports={
    context:path.resolve(__dirname, 'app')
}

  1. 命令行式改变
webpack --context path

Entry 类型

entry可以是三种类型:

  • String:'./app/entry',只输出一个Chunk,Chunk的名称是main
  • Array:['./app/entry1', './app/entry2'],只输出最后一个元素的 Chunk,Chunk的名称是main
  • Object:{a:'./app/entry-a',b:'./app/entry-b'},输出多个Chunk,Chunk的名称是对象对应的键值

动态配置 Entry

当入口文件是动态时,就需要给 Entry 传入函数,如下配置:

// 同步函数
entry:()=>{
    return {
        a:'./app/entry-a',
        b:'./app/entry-b'
    }
}
// 或者异步函数
entry:()=>{
    return new Pormise((resolve)=>{
        resolve({
        a:'./app/entry-a',
        b:'./app/entry-b'})
    })
}

Output

常见的属性有:

  • filename:配置输出文件的名称
  • path:配置输出文件的目录
  • publicPath:用于处理静态资源引用地址

filename 和 path

比如:

output:{
    filename:'bundle.js',
    path:path.resolve(__dirname, './dist')
}

filename 除了可以配置静态名称也可以配置动态名称:

  • filename: [id].js:chunk的唯一标识符,从0开始
  • filename: [name].js:chunk的名称
  • filename: [hash].js:chunk 唯一标识hash,可以取hash前几位,比如取前8位:filename: [hash:8].js
  • filename: [chunkhash].js:chunk 内容的 hash,可以取hash前几位,比如取前8位:filename: [hash:8].js

其他属性

  • chunkFilename
  • publicPath
  • crossOriginLoading
  • libraryTarget和 library
  • libraryExport
  • 等等…

Module

主要用来配置模块处理功能,里面主要有 rules 属性,Array 类型,用于加载和解析模块文件,用于配置 Loader 规则。

rules 元素有以下几个常用的属性:

  • test
  • use
  • noParse
  • parser

配置 Loader

通过以下的示例来说明 Loader 配置。

module: {
    rules: [
      {
        // 命中 js 文件
        // test 也可以接收数组类型,其中每个元素条件之间是“或”的关系
        test:/\.js$/,
        // 用 babel-loader 转换 JavaScript 文件
        // ?cacheDirectory 表示传给 babel-loader 的参数,用于缓存 babel 的便宜结果,加快编译速度
        // use 数组里的loader执行顺序为从右向左,也可以通过enforce强制设置执行位置。
        use:['babel-loader?cacheDirectory'],
        // 只命中 src 目录里的 JavaScript 文件,加快 Webpack 的搜索速度
        include:path.resolve(__dirname,'src'),
        // 通过 exclude 排除 node_modules 目录下的文件,
        // 一般来讲,include和exclude不必要同时配置,在此为了说明两者用法,故同时配置
        exclude: path.resolve(__dirname,'node_modules'),
        // 配置哪些内置模块语法被解析
        parser: {
          amd:false, // 禁用 AMD
          commonjs:false, // 禁用 CommonJS
          system:false, // 禁用 SystemJS
          harmony:false, // 禁用ES6 import/export
          // 等等...
        },
      }
    ],
    // 忽略符合以下规则模块的解析, noparse 可以是 RegExp,[RegExp],function中的一种
    noParse: /jquery|chartjs/
  }

Resolve

用来告知 webpack 按照何种规则来寻找模块。

可由以下示例来讲解:

resolve: {
    // 配置别名
    alias: {
      '@':'./src/components',
    },
    // 决定优先是哪个入口文件代码,查找顺序从左至右
    mainFields: ['browser','main'],
    // 配置扩展名,当导入文件的扩展名省略时,从以下数组中依次匹配查找,查找顺序从左至右
    extensions: ['.ts','.js','.json'],
    // './src/components' 和 'node_modules' 路径下的模块导入的相对路径可以其作为根目录
    // 比如 './src/components' 的button模块,直接可以通过 import 'button' 导入
    modules:['./src/components', 'node_modules']
    // 配置第三方模块文件名称描述,也就是 package.json
    descriptionFiles: ['package.json'],
    // 模块导入时是否必须带扩展名
    enforceExtension: true,
    // 模块导入时是否必须带扩展名,只针对 node_modules 下的模块生效
    enforceModuleExtension: false
  },

Plugin

Plugin 用于扩展 webpack,配置很简单,接收数组类型,每个元素都是 Plugin的示例,Plugin的参数由构造函数传入,比如:

const ExtractTextPlugin = require('extract-text-webpack-plugin')

module.exports = {
    plugins:[new ExtractTextPlugin({
        // 提取出来的 css 文件名
        filename: `style.css`
  	})]
}

难点在于各自 Plugin 自身的参数配置。

DevServer

DevServer 除了在命令行配置参数,也可以在配置文件webpack.config.js中配置

module.exports={
    devServer: {
    	host:'127.0.0.1',
        port:8080
  	}
}

此配置项仅针对 devServer 命令有效,webpack 命令会忽略此配置项。

之前也讲了它的简单用法,以下讲下其常用的配置:

  • hot:用于热替换
  • host:用设置web服务器地址
  • port:用于设置端口号
  • https:用于设置https服务,默认是http服务。
  • open:在第一次构建完是否自动打开网页

其他配置项

  • devtool:用于设置 source-map,便于调试
  • watchwatchOptions:用于监听文件改动,webpack-dev-server 命令自动开启
  • externals:告知以下文件无需打包。
  • 等等…

实战

具体配置可查看书籍或相关Loader介绍。

优化

优化方向有两个:

  • 优化开发效率(针对开发人员)
    • 提高构建速度
    • 优化开发体验
  • 优化输出质量(针对用户)
    • 减少首屏加载时间
    • 提升流畅度

缩小文件的搜索范围

  • 优化 Loader 配置:针对 module 配置
    • 可以添加 noParse 属性,排除一些非模块化文件,像 noparse:/jquery|chart\.js/。确保这些文件中没有模块化操作。
    • 在 rules 属性中给其 Loader 尽可能添加 include 或 exclude 属性以缩小解析范围。
  • resolve 配置:
    • extensions 的数组长度尽可能的短,频率高的后缀名放前面。
    • 尽可能的在 alias 中配置依赖模块的具体位置。
    • 等等…

使用 HappyPack

使用 HappyPack 插件进行多进程构建。它会接管某些 Loader 任务,并行执行这些任务。

使用 parallelUglifyPlugin 插件

webpack 内置了 UglifyPlugin 用于压缩代码,parallelUglifyPlugin 插件可以多进程并行压缩代码。

使用自动刷新优化开发体验

主要通过配置 watch 实现。

监听文件是否发生改变,如果发生改变则在一定时间内重新构建输出文件,并通过向网页中注入代理客户端代码的方式来实现自动刷新功能。

开启模块热替换优化开发体验

原理:只需编译发生变化了的代码,再将新的输出模块替换浏览器中旧的模块。

优势:

  • 构建速度快。
  • 可以在保存浏览器状态的情况下更新页面。

区分环境

通过 DefinePlugin 插件来定义 process.env.NODE_ENV 的值。

压缩代码

JavaScript 压缩原理:插件通过分析 JavaScript 语法分析树,按照一定的规则将代码中的输出日志、注释等一些无用代码去除。

CSS 压缩原理:理解 CSS 含义,比如会将 color:#ff0000 压缩成 color:red,压缩率可达到 60%。

  • 压缩 ES5
    • 可用之前说的 UglifyPluginParallelUglifyPlugin 插件进行配置,也可以启动 webpack 时带上 --optimize-minimize 参数,它会自动配置UglifyPlugin 插件的默认参数。
  • 压缩 ES6
    • 可用 uglifyESPlugin 插件进行压缩,需要注意的是压缩 ES6 代码需要运行环境支持ES6语法才有意义。
  • 压缩 CSS
    • 通过给 css-loader Loader添加 minimize 参数进行压缩:use:['css-loader?minimize']

CDN 加速

将不同的静态资源放在不同域名的CDN下,可以利用webpack进行配置

使用 Tree Shaking

Tree Shaking 插件是为了剔除没有用到的死代码。

使用 Tree shaking 的前提是模块化必须是 ES6 语法,因为其导入导出的路径必须是静态字符串,不能出现在代码块中。像 CommonJS 就不一样:require(x+y)

提取公共代码

为什么要提取?

因为如果每个页面都包含大量公共代码的话,会导致:

  • 相同的资源被重复加载,浪费用户流量和服务器成本
  • 每个页面需要加载的资源过大,影响页面渲染

如何提取?

先将所有页面中用到的基础依赖库(如react,react-dom)提取到单独的一个文件 base.js 中。然后再从所有页面中提取出不包含base.js中代码的公共代码到common.js文件中,然后将每个网页剩余的代码单独包装成一个文件。

如图:

在这里插入图片描述

提取公共代码可利用 webpack 内置的 commonsChunkPlugin 插件

分割代码以按需加载

像现在流行的单页应用,将所有的功能整合到了一个 HTML 文件中。其实每次只是运用到其中的一部分功能,为了使用一部分功能而加载所有资源而使得网页加载相对缓慢。为了解决此问题,则可以将整个页面分成一个个小的功能,而根据功能之间的相关程度进行分类,把每一类合并成一个chunk,然后再按需加载每一个Chunk。

可以使用 import(*) 语句实现按需加载。

使用 Prepack

通过深入分析JavaScript代码逻辑,预先执行代码逻辑,然后将执行结果输出,此技术还未成熟。

比如:

// 转换前
function hello(){
	console.log("hello,world")
}
hello();

// 转换后
console.log("hello,world")

开启 Scop Hoisting

可以进一步的减少打包体积,通过将多个函数进行合并,但是前提是运用ES6语法的模块。

可利用 webpack 内置的功能进行开启

输出分析

如果需要分析webpack 的详细输出信息,可在命令行添加以下参数

  • --profile :记录构建过程中的耗时信息
  • --json:以 json 格式记录输出文件信息

如:webpack --profile --json > state.json

> 为 LINUX 中的管道命令,就是将输出结果存到 state.json 文件中

可以通过以下两种方式来可视化分析 state.json 文件信息

  • Webpack Analyse:一个web应用,登录其官网上传 json 文件即可分析
  • webpack-bundle-analyzer:一个全局插件,在 state.json 所在目录下输入webpack-bundle-analyzer即可分析 state.json

原理

工作原理概括

核心概念

  • Entry:构建入口。
  • Ouput:构建输出信息。
  • Module:模块,在 webpack 中每个文件都当作是一个模块。
  • Chunk:代码块,有多个模块组成,用于代码合并与分割。
  • Loader:模块转换器,Loader 根据需求将原有文件转成新的内容。
  • Plugin:Webpack 扩展插件,webpack 在构建的过程中会广播一些事件,而 Plugin 则通过监听相应的事件来扩展某些功能。

构建流程

主要分三大阶段:

  • 初始化:启动构建,读取合并 webpack 配置参数,加载插件,初始化 compiler 对象。
  • 编译:读取 Entry 配置中的模块,根据匹配规则调用相应的 Loader 对其进行转换,并找到其依赖的模块进行递归解析转换,并确定各个模块之间的依赖关系。
  • 输出:将有联系的多个Module合并成一个chunk,再将每个chunk输出成一个文件并保存在系统中,保存信息由 Output 配置决定。

编写 Loader

简单来说输出一个转换函数。

Npm Link 可用于调试开发本地的 Npm 模块。

编写 Plugin

以下有些常用的属性和function总结如下:

  • compilation.chunk:数组类型,存放所有的 chunk
  • chunk.forEachModule(module=>{}):遍历每个 module
  • module.fileDependencies:数组类型,表示当前模块所依赖的文件路径
  • chunk.files:数组类型,表示 chunk 输出的文件,有可能不只一个文件。
  • compilation.assets[filename].source():表示 filename 输出文件的内容
  • compilation.fileDependencies:数组类型,表示文件依赖列表
  • compiler.options.plugins:当前配置使用的所有插件列表
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章