关于javascript:大前端进阶模块化

40次阅读

共计 16997 个字符,预计需要花费 43 分钟才能阅读完成。

概述

模块化是一种解决问题的计划,一个模块就是实现某种特定性能的文件,能够帮忙开发者拆分和组织代码。

js 模块化

JavaScript 语言在设计之初只是为了实现简略的性能,因而没有模块化的设计。然而随着前端利用的越来越宏大,模块化成为了 js 语言必须解决的问题。

模块化倒退

js 的模块化倒退大抵能够划分为四个阶段:

  • 文件划分

依照 js 文件划分模块,一个文件能够认为是一个模块,而后将文件通过 script 标签的形式引入。
编写模块:foo.js

var foo = 'foo'
function sayHello() {console.log(foo)
}

应用模块:

<html>
<header></header>
<body>
    <!-- 先援用 -->
    <script src="./foo.js"></script>
    <script>
        // 通过全局对象调用
        window.sayHello()
    </script>
</body>
</html>

文件划分形式无奈治理模块的依赖关系(不是强制定义模块依赖),而且模块内所有变量都挂载在全局对象上,容易净化全局作用域,命名抵触。

  • 命名空间

将文件内所有的变量都增加到一个命名空间下。
编写模块:

var FooModule = {
    foo: 'foo',
    sayHello() {console.log(FooModule.foo)
    }
}

应用模块:

<script>
    // 通过命名空间调用
    FooModule.sayHello()
</script>

应用命名空间的益处是能够尽量避免命名抵触,然而因为命名空间挂载在全局对象下,仍然可能在内部批改模块的变量(没有实现模块私有化)。

  • 立刻执行函数

利用函数作用域,将模块门路包裹在一个立刻执行函数中,能够指定须要裸露给内部的变量。
编写模块:

;(function (w) {
    var foo = 'foo'
    w.sayHello = function () {console.log(foo)
    }
})(window)

应用模块:

<script>
    // 通过命名空间调用
    window.sayHello()
</script>

自执行函数利用函数作用域实现了变量私有化。

  • 模块化标准

ES2015 提出了规范模块化标准,即 ES Modules。它蕴含一个模块化规范和一个模块加载器。
编写模块

// moduleA.js
export const foo = 'foo'

// moduleB.js
// 会主动从服务器下载 moduleA.js 文件
import {foo} from './moduleA.js'
console.log(foo)

应用模块

<html>
<header></header>
<body>
    <!-- 引入 moduleB.js-->
    <script type="module" src="./moduleB.js"></script>
</body>
</html>

注意事项:

  1. 引入模块 js 时,必须增加 type=module
  2. 因为模块会主动下载依赖文件,因而 html 文件必须挂载到服务器下,间接文件浏览会报错。

模块化标准

目前,JavaScript 语言大抵上有三种模块化标准:CommonJs,AMD,ES Modules

CommonJs

CommonJs 是 Nodejs 中应用的模块化标准,它规定一个文件就是一个模块,每个模块都有独自的作用域,模块中通过 require 引入模块,通过 module.exports 导出模块。

// moduleA.js
module.exports = {foo: 'foo'}

// moduleB.js
const {foo} = require('./moduleA.js')
console.log(foo)

能够在命令行中通过 node moduleB.js 运行。

AMD

AMD 是浏览器端规定异步模块定义的标准,通常配合 requirejs 应用。

// 通过数组引入依赖,回调函数通过形参传入依赖
define(['ModuleA', 'ModuleB'], function (ModuleA, ModuleB) {function foo() {
        // 应用依赖
        ModuleA.test();}
    // 导出模块内容
    return {foo: foo}
})

ES Modules

ES Modules 是 ECMAScript 提出的规范模块标准,次要利用在浏览器端,目前并不是所有浏览器均反对该个性。

ES Modules

根本个性

  • script type=module

在 html 中能够通过 script 标签援用,须要应用 type=module 通知浏览器加载的 js 文件是一个模块,浏览器会主动下载模块中的依赖模块。

  • 主动采纳严格模式

如果某个 js 文件通过模块的形式被浏览器引入,那么该 js 文件会主动变成严格模式,也就是在 js 文件中能够省略use strict

  • 运行在独自的公有作用域中

运行在独自的公有作用域中保障了命名不会抵触。
module.js 中:
var foo = 'foo'
index.html 中:

<html>
<header></header>
<body>
    <script type="module" src="./module.js"></script>
    <script>
        // 即便模块中的 foo 变量应用的是 var 申明的,此时也不能在全局作用域中找到 foo 变量
        console.log(foo)
    </script>
</body>
</html>
  • 通过 CORS 形式申请内部 js 文件

如果 script 标签的 src 属性值是一个 url 地址,那么这个地址必须容许 CORS 跨域拜访。
<script type="module" src="https://dss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/static/protocol/https/jquery/jquery-1.10.2.min_65682a2.js"></script>
上例中,因为 dss1.bdstatic.com 不容许跨域拜访,因而会报错。

  • 主动提早执行

通过模块形式引入的 js 代码会被浏览器提早执行。
module.js 中:
console.log('module')
index.html 中:

<html>
<header></header>
<body>
    <script type="module" src="./module.js"></script>
    <script>
       console.log('out module')
    </script>
</body>
</html>

此时会先打印 out module,再打印模块外部的 module。
成果等同于在 script 标签上加上 defer 属性:

<html>
<header></header>
<body>
    <script defer src="./module.js"></script>
    <script>
       console.log('out module')
    </script>
</body>
</html>

导入导出

  • export {…} 是一种语法,不是对象字面量。
const foo = 'foo'
// 此处并不是对象字面量
export {foo}
// 如果是对象字面量,那么应该反对如下写法
// 实际上,这样写会报错
export {foo: 'foo'}
  • import 导入之后不能再扭转变量
import {foo} from './moduleA.js'
// 不容许扭转援用的变量
foo = '123'
  • import 能够导入相对路径,绝对路径和 url

相对路径
import {foo} from './moduleA.js'
绝对路径
import {foo} from '/moduleA.js'
url:

import {foo} from 'http://localhost:8080/moduleA.js'
  • import 前面间接根文件门路,此时是只导入,不援用。

如果某个模版文件 module.js 中没有通过 export 导出成员,那么能够通过 import ” 的形式导入模块。

import './moduleA.js'
  • import 动静导入

如果模块中须要在运行的时候才晓得导入模块地址或者在某个逻辑下才导入某个模块,那么 import from 的形式就会报错。
谬误导入:

// 地址不明确(开发阶段)const moduleA = './moduleA.js'
import {foo} from moduleA
// 在某些逻辑中导入成员
if(true) {import { foo} from './moduleA.js'
}

这种状况下,能够应用 Modules 提供的 import 函数,该函数返回一个 promise 对象,因为是个函数,所以能够在任何中央应用。

const moduleA = './moduleA.js'
import(moduleA).then(module => {
    // module 中蕴含模块所有的导出成员
    console.log(module.foo)
})
  • import 同时导入默认成员和具名成员

在某个模块中,如果须要同时导入默认成员和具名成员,能够以如下形式导入:

import {foo, default as sayHi} from './moduleA.js'
// 或者
import sayHi, {foo} from './moduleA.js'
  • 间接导出导入成员

在某些模块文件中,可能须要从别的模块导入某个成员,而后在这个模块间接导出这个成员。
失常写法:

import {foo} from './moduleA.js'
export {foo}

简略写法:

export {foo} from './moduleA.js'

运行环境兼容

浏览器

目前,并不是所有浏览器都反对 ES Modules 个性,如 IE。利用 nomodule 能够实现优雅降级。

<html>
<header></header>
<body>
    <script type="module">
        import module from './module.js'
    </script>
    <script nomodule>
        alert('你的浏览器版本不反对 ES Modules')
    </script>
</body>
</html>

在反对 modules 的浏览器中,会运行 type=’module’ 的脚本,在不反对的浏览器中,会疏忽模块文件,并运行 nomodule 对应的 script 脚本。

nodejs

nodejs 在 8.0 版本开始反对 ES Modules。想要在 nodejs 中应用,须要满足两个条件:

  • 文件扩大名为.mjs
  • 运行时须要加 –experimental-modules 参数
// moduleA.mjs
export const foo = 'foo'
export default function(){console.log(foo)
}
// moduleB.mjs
import {foo} from './moduleA.mjs'
console.log(foo)

此时通过命令行运行 node .moduleB.mjs –experimental-modules,能够失常工作。

commonjs 交互

在 mjs 的文件中,能够导入 commonjs 定义的模块,始终导入一个默认对象。

// moduleA.js
module.exports = {foo: 'foo'}
// moduleB.mjs
import * as moduleA from './moduleA.js'
console.log(moduleA.foo)

反过来,不能在 commonjs 定义的模块中导入 ESModules 定义的对象。

在最新的 nodejs 中能够在 package.json 中增加 type:’module’ 属性,此时,模块文件的扩展名就不须要再应用 mjs,然而相应的,应用 commonjs 定义的文件扩展名须要为 cjs。

区别

在 commonjs 定义的模块文件中,能够应用 requie,module,exports,__filename, __dirname 全局对象,然而 ESModules 模块文件中没有这些全局对象,能够应用 import.meta 属性中的某些属性获取相应的值。

// 能够应用门路解析获取 filename 和 dirname
console.log(import.meta.url)

模块化打包

在浏览器环境中间接应用 ESM(ES Modules)个性,会呈现如下问题:

  • 并不是所有浏览器都反对 ESM 个性。
  • 模块化文件过多会导致网络申请频繁。
  • ESM 只反对 js 文件模块化,css、图片、字体等文件不反对模块化。

为了解决上述问题,就呈现了模块化打包工具。此类工具会让开发者在开发阶段应用模块组织资源、代码等,在上线前,通过打包,从新组织模块化的文件,以解决上述问题。

webpack

目前,最罕用的模块化打包工具就是 webpack,通过 webpack 能够疾速实现模块化打包。

装置依赖:npm install webpack webpack-cli --save-dev
执行打包:npm run webpack
webpack 插件会主动认为当前目录下的 src/index.js 为打包入口文件,查找所有依赖并打包到 dist/main.js 中。
当然,webpack 也反对配置文件,能够在我的项目根目录下增加 webpack.config.js 文件:

const path = require('path')
module.exports = {
    // 指定打包入口文件
    entry: './src/index.js',
    output: {
        // 打包输入文件名
        filename: 'bundle.js',
        // 打包输出文件夹,必须应用绝对路径
        path: path.join(__dirname, 'dist')
    }
}

利用配置文件能够批改 webpack 的默认配置。

工作模式

webpack 的工作模式分为三种:node, development,production。能够通过设置工作模式,以应答不同的打包需要。webpack 默认应用 production 模式打包,会主动优化打包后果。

在配置文件中设置模式:

const path = require('path')
module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
    mode: 'none' // 'development production'
}

development:打包后的代码和开发代码一样,可读性强,不会主动优化。
none: 删除 webpack 打包过程中生成的正文代码,其余和 development 雷同。
production:打包后的代码会主动优化。

loader

在 webpack 中万物皆可模块,只是 webpack 内置了如何解决 js 代码,其余资源如 css,图片等须要应用相应的 loader 进行转换。

因为 webpack 默认利用是由 js 驱动的,因而想要打包其余资源文件,须要在 js 代码中建设与其余资源文件的分割,即导入。

import 'logo.png'
import 'common.css'

css

能够利用 css-loader 和 style-loader 配置解决导入的 css 文件。

原理是 css-loader 将 css 代码转换为 js 模块(将 css 中的内容放到一个数组中并导出)。style-loader 获取转换后的字符串并转换为 style 节点增加到 html 文件的 header 节点中。

装置依赖:npm install –save-dev css-loader style-loader。
增加配置:

module.exports = {
  // ...
  module: {
    rules: [
      {
        // 通知 webpack,以 css 结尾的文件须要通过这里配置的 loader 进行转换。test: /.css$/,
        // 转换用的 loader,执行程序自后向前
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  }
}

图片

图片也是一种资源文件,须要 loader 进行解决。能够利用 file-loader 解决图片资源,原理是将图片独自导出为一个文件,而后在 js 模块中导出转换后的图片门路。
加载依赖:npm install –save-dev file-loader。
增加配置:

const path = require('path')
module.exports = {
  // ...
  module: {
    rules: {
      {
        test: /.png$/,
        use: 'file-loader'
      },
      //...
    ]
  }
}

当然,也能够利用 Data URLs 协定,该协定容许在文档中嵌入小文件。
Data URLs 由四个局部组成:

  • 前缀(data:)
  • 批示数据类型的 MIME 类型
  • 如果非文本则为可选的 base64 标记
  • 数据自身
data: [<mediatype>][;base64], <data>

如果应用该协定,那么能够利用 url-loader,该 loader 能够将图片资源转换为 url。针对大文件,能够设置 limit 属性,当超过 limit 限度的大小后,url-loader 将图片作为独自的文件打包。
增加依赖:npm install –save-dev url-loader
增加配置:

const path = require('path')

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            // 增加文件大小限度
            limit: 10 * 1024 // 10 KB
          }
        }
      },
      // ...
    ]
  }
}

触发机会

既然所有资源都能够通过 loader 进行模块化解决,那么在什么状况下,webpack 会将资源辨认为一个模块呢?
如下状况会被辨认:

  • ES Modules import 导入
  • commonjs require 导入
  • amd 模式下的 define 和 require
  • html 节点中的 src 属性
  • css 文件中的 import 和 url

如果想要辨认 html 中的 src 属性,须要配合 html-loader 应用:

{
    test: /.html$/,
    use: {
      loader: 'html-loader',
      options: {
        // 指定哪些 attr 会被辨认为模块资源
        attrs: ['img:src', 'a:href']
      }
    }
}

ES 个性转换

如果在 js 代码中应用了 ES 的新个性,webpack 自身并不会转换这些个性,须要应用 babel-loader。
加载依赖:npm install –save-dev babel-loader @babel/babel-core @babel/preset-env。
增加配置:

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.js$/,
        use: {
          loader: 'babel-loader',
          options: {presets: ['@babel/preset-env']
          }
        }
      },
      // ...
    ]
  }
}

自定义 loader

webpack 提供了大量的用于转换的 loader,loader 的实质是实现资源文件输出和输入之间的转换。在特定状况下,咱们须要本人定义符合要求的 loader,字定义 loader 文件默认导出一个函数,函数的参数是读取到的文件内容或者是另一个 loader 解决后的内容。
上面是一个转换 md 文件的自定义 loader:

const marked = require('marked')
module.exports = source => {
  // 利用 marked 将 md 文档转为 html 可辨认的字符串
  const html = marked(source)
  // 须要返回 js 能辨认的模块字符串
  // return `module.exports = "${html}"`
  // return `export default ${JSON.stringify(html)}`
  // 或者返回 html 字符串交给下一个 loader 解决
  return html
}

增加配置:

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.md$/,
        use: [
           // html-loader 将 markdown-loader 返回的 html 字符串转换为一个模块
          'html-loader',
          './markdown-loader'
        ]
      }
    ]
  }
}

Plugin

loader 实现了资源文件的转换,相比于 loader,plugin 能够实现其余自动化工作,如清空输入文件夹、主动在 html 中注入打包后的 js 文件等。

plugin 领有更宽的能力范畴,通过在 webpack 打包生命周期中挂在函数实现 webpack 扩大。

清空输入文件夹

增加 clean-webpack-plugin 插件,能够在每次打包之前主动清空上次的打包后果。
增加依赖:npm install –save-dev clean-webpack-plugin。
增加配置:

const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {
  // ...
  module: {
    rules: [// ...]
  },
  plugins: [new CleanWebpackPlugin()
  ]
}

主动生成 html

通过 html-webpack-plugin 插件可主动在打包输入文件夹下生成 html 文件,生成的 html 文件中可实现如下自动化性能:

  • 主动增加打包后的 js 文件。
  • 增加字定义 meta 属性。
  • 可利用模板编译,主动退出变量。

增加依赖:npm install –save-dev html-webpack-plugin。
增加配置:

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  // ...
  module: {
    rules: [//...]
  },
  plugins: [
    // ...
    // 用于生成 index.html
    new HtmlWebpackPlugin({
      // 模板编译,替换模板文件中的 `<%= htmlWebpackPlugin.options.title %>`
      title: 'Webpack Plugin Sample',
      // 增加 meta 头
      // 相当于在 html header 中增加 `<meta name="viewport" content="width=device-width">`
      meta: {viewport: 'width=device-width'},
      // 指定模板文件
      template: './src/index.html'
    }),
    // 能够增加多个 HtmlWebpackPlugin,用于生成多个 html 文件
    // 用于生成 about.html
    new HtmlWebpackPlugin({filename: 'about.html'})
  ]
}

复制动态资源

在 public 文件夹下的诸如 favicon.ico 文件是不须要打包的,能够间接复制到输入目录下。
增加依赖:npm install copy-webpack-plugin –save-dev。
增加配置:

const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  // ...
  module: {
    rules: [// ...]
  },
  plugins: [
    // 指定间接复制的文件门路
    new CopyWebpackPlugin([
      // 'public/**'
      'public'
    ])
  ]
}

自定义 plugin

尽管 webpack 提供了大量的 plugin 插件用于实现日常开发工作,然而某些状况下,须要咱们增加字定义 plugin。

字定义 plugin 是一个函数或者是一个蕴含 apply 办法的类。

上面的例子是主动删除打包后 js 文件中每一行结尾的/******/

class MyPlugin {apply (compiler) {
    // 注册生命周期函数
    // 此例中的 emit 是在实现打包后,将要输入到输入目录的节点执行。// compilation 是此次打包的上下文,蕴含所有的打包的资源文件。compiler.hooks.emit.tap('MyPlugin', compilation => {for (const name in compilation.assets) {
        // 通过 name 属性能够获取文件名称
        if (name.endsWith('.js')) {
          // 通过 source 办法获取相应内容
          const contents = compilation.assets[name].source()
          const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
          // 替换原有的内容,须要实现 source 办法和 size 办法。compilation.assets[name] = {source: () => withoutComments,
            size: () => withoutComments.length}
        }
      }
    })
  }
}

增加配置:

module.exports = {
  // ...
  module: {
    rules: [// ...]
  },
  plugins: [new MyPlugin()
  ]
}

加强开发体验

通过上述的 loader 和 plugin 能够实现我的项目的打包工作,然而咱们须要在开发时可能加强开发体验,如主动编译,主动刷新,不便调试等。

主动编译

webpack 内置了主动编译性能,其能够主动监听文件变动,主动打包运行。能够在命令执行的时候增加 –watch 实现。

主动刷新浏览器

当 webpack 可能主动打包后,咱们心愿在开发环境下可能主动刷新浏览器。webpack 提供了 webpack-dev-server 插件,能够实现此性能。

装置依赖:npm install webpack-dev-server –save-dev。
在命令行中运行:npm run webpack-dev-server –open 即可实现主动关上浏览器,主动刷新浏览器。

浏览器中关上的资源指向的 webpack 输入的目录,然而在开发阶段,public 中的动态文件并没有被打包进去,此时会造成资源失落,能够利用配置文件中的 devServer 属性实现此性能配置。

devServer: {
    // 指定其余动态资源文件地址
    contentBase: './public'
    // 还能够利用 proxy 实现开发阶段的服务端代理。}

主动刷新浏览器尽管解决了局部开发优化问题,然而主动刷新会导致页面状态全副失落(在 input 中输出测试文字,刷新后测试文字没有了,须要再次手动输出),这样还不是很敌对。

为了解决刷新导致的页面状态失落问题,webpack 还提供了 HRM 热更新,热更新能够保障模块发生变化后,页面只会替换相应变动的局部,不会导致状态失落。

在 webpack 中启动热更新,能够增加 –hot 参数。

测试发现,HRM 能够热更新 css 和图片等资源文件,然而针对 js 文件,无奈做到主动替换,还是须要刷新浏览器,这种状况下,须要咱们手动增加热更新解决代码。

例如在某个模块中,当依赖模块发生变化(页面中的某个元素发生变化),能够通过如下代码监控代码变动,并手动实现热更新性能:

let hotEditor = editor
module.hot.accept('./editor.js', () => {
    // 获取元素的状态:即获取用户曾经输出的内容
    const value = hotEditor.innerHTML
    // 移除旧有的页面元素
    document.body.removeChild(hotEditor)
    // 创立一个变动后的元素
    hotEditor = createEditor()
    // 将移除之前存储的状态赋值给新的元素
    hotEditor.innerHTML = value 
    // 将新的元素增加到页面上
    document.body.appendChild(hotEditor)
})

其余和热更新相干的:

–hotOnly: 应用这个代替 –hot 参数能够屏蔽热更新代码中的谬误,热更新代码只是辅助开发用的,如果其中呈现谬误并不需要被控制台输入。

module.hot: 在模块中能够通过判断 module.hot 来获取以后我的项目是否开启了热更新,如果没有开启,那么代码打包过程中会主动删除热更新逻辑,不影响生产环境。

source-map 调试

webpack 打包后的代码不利于开发阶段调试,因而须要 source-map 来定位谬误,解决源代码和运行代码不统一导致的问题。

感受一下 source-map 的魅力:
在浏览器控制台输出:eval(‘console.log(“foo”)’)

红色框中显示的是代码在哪执行,这个显示很不敌对。
再次输出:eval(‘console.log(123) //# sourceURL=foo.js’)

通过增加 sourceURL 就能够通知控制台这个代码是在哪个文件中执行的。

webpack 中通过简略的配置即可实现 source-map:
devtool: 'eval',
其中 devtool 的值是 source-map 的类型,webpack 反对 12 中 source-map 类型:

通常状况下,打包速度快的,调试成果个别都不好,调试成果好的,个别打包速度比较慢,在我的项目中具体应用哪种类型,须要本人去斟酌。

  • eval

模块中的代码通过 eval 去执行,在 eval 的最初增加 sourceURL,并没有增加 source-map,只能定位哪个文件中呈现谬误。

  • eval-source-map:

在 eval 的根底上增加了 source-map,能够定位谬误的具体行列信息。

  • cheap-eval-source-map

阉割版的 eval-source-map,只能定位到行,无奈定位列信息。

  • cheap-module-eval-source-map

在 cheap-eval-source-map 的根底上,能够保障定位的行信息和源文件的行绝对应。

  • inline-source-map

一般的 source-map 中,map 文件是物理文件,而 inline-source-map 模式下,map 文件是以 Data URLs 的模式打包到文件的开端。

  • hidden-source-map

生成了 map 文件,然而打包后的开端没有增加该 map 文件,保障了源代码不会裸露,同时在调试时,能够手动将 map 文件增加到文件开端进行调试。

  • nosources-source-map

能够定位谬误的行列信息,然而无奈在开发工具中看到源代码。

生产环境优化

生产环境和开发环境的关注点是不一样的,开发环境重视开发效率,生产环境重视运行效率。

不同环境,不同配置文件

webpack 激励为不同的环境设置不同的配置文件,能够通过以下两种形式实现。

  • 在一个配置文件中,通过判断不同环境导出不同的配置。
  • 增加多个配置文件,指定 webpack 运行时的配置文件。

同一个配置文件中,导出一个函数,此函数返回一个配置对象:

const webpack = require('webpack')
module.exports = (env, argv) => {
  const config = {// ...}
  // 判断是那种环境
  if (env === 'production') {
    config.mode = 'production'
    config.devtool = false
    config.plugins = [
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin(['public'])
    ]
  }
  return config
}

多个配置文件:
增加专用配置文件:webpack.common.js

module.exports = {// ....}

增加生产环境配置文件

const merge = require('webpack-merge')
const common = require('./webpack.common')
// 应用 webpack-merge 实现配置文件的合并
module.exports = merge(common, {
  mode: 'production',
  plugins: [new CleanWebpackPlugin(),
    new CopyWebpackPlugin(['public'])
  ]
})

应用时,通过 –config 参数指定配置文件。

DefinePlugin

能够利用 webpack 内置的 DefinePlugin 为代码注入全局成员,打包时,webpack 会主动利用指定的值替换代码中呈现的全局成员。
定义成员:

const webpack = require('webpack')
module.exports = {
  // ...
  plugins: [
    new webpack.DefinePlugin({
      // 值要求的是一个代码片段
      API_BASE_URL: JSON.stringify('https://api.example.com')
    })
  ]
}

应用成员:

console.log(API_BASE_URL)

打包后:

console.log('https://api.example.com')

合并

模块打包会导致最终输入的文件夹中模块文件过多,能够利用模块合并尽可能的将模块合并到一个函数中,缩小模块数量。
启用合并:

module.exports = {
  // ...
  optimization: {
    // 尽可能合并每一个模块到一个函数中
    concatenateModules: true,
  }
}

Tree-shaking

Tree-shaking 指的是去除代码中未援用的代码,也就是无用代码。通过去除无用代码,能够缩小代码文件体积,优化加载速度,webpack 默认在 production 模式下启动 Tree-shaking。

webpack 中没有明确的某个配置用于启动 Tree-shaking,它是一系列配置一起实现的性能。

module.exports = {
  // ...
  optimization: {
    // 打包后的模块只导出被应用的成员
    usedExports: true,
    // 压缩输入后果,在压缩的过程中后主动删除未被导出的代码
    minimize: true
  }
}

有的时候,人们会认为 Tree-shaking 和 babel 转换相冲突,也就是用了 babel 转换会导致 Tree-shaking 失败。

其实,二者是不抵触的,Tree-shaking 依赖的是 ESM,通过对 import 的剖析达到去除无用代码的成果。babel 转换的时候会默认将 ESM 编写的模块转换为 Commonjs 标准的模块,因而会导致 Tree-shaking 失败。

通过为 babel-loader 的 presets 增加配置能够让 babel 转换的时候不将 ESM 转换为 Commonjs:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              // 如果 Babel 加载模块时曾经转换了 ESM,则会导致 Tree Shaking 生效
              // ['@babel/preset-env', { modules: 'commonjs'}]
              // ['@babel/preset-env', { modules: false}]
              // 也能够应用默认配置,也就是 auto,这样 babel-loader 会主动敞开 ESM 转换
              ['@babel/preset-env', { modules: 'auto'}]
            ]
          }
        }
      }
    ]
  }
}

副作用

副作用指的是模块除了导出成员之外,还进行了其余操作。如在模块中引入了 css 文件,这个引入过程并没有应用外部成员,因而在 Tree-shaking 的时候就会被主动去掉。

为了防止因为 Tree-shaking 去掉导致我的项目运行失败,须要进行副作用代码标记。

增加启用副作用配置:

module.exports = {
  // ...
  optimization: {sideEffects: true,}
}

在 package.json 中指定副作用文件地址:

"sideEffects": [
    "./src/extend.js",
    "*.css"
]

指定地位的文件不会被 Tree-shaking 当作无用代码删除。

代码宰割

如果将所有的资源都打包到一个文件中,那么这个文件会过大,导致加载工夫过长,影响我的项目体验,此时,须要依据状况,对我的项目打包进行代码宰割,代码宰割通常随同多入口打包。

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  // 提供多个代码打包入口
  entry: {
    // 将 index.js 入口的所有文件打包到 index 的 chunk 中。index: './src/index.js',
    album: './src/album.js'
  },
  plugins: [
    // 针对多个入口生成多个 html 文件
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      // 指定 html 文件依赖的 chunk
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album']
    })
  ]
}

提取公共模块

在代码宰割时,如果多个入口文件依赖一些专用代码,这些专用代码被打包到每个文件中,会减少文件体积,此时须要提取公共模块到一个独自文件中。
增加配置:

module.exports = {
  optimization: {
    splitChunks: {
      // 主动提取所有公共模块到独自 bundle
      chunks: 'all'
    }
  }
}

按需加载

如果在我的项目启动时,加载所有模块,那么会因为申请过多导致加载工夫长,此时能够利用动静导入模块的形式实现按需加载,所有动静导入的模块会主动分包。

import(
// webpackChunkName 是一种 webpack 中的魔法正文,通过魔法正文,能够指定动静导入的模块打包后生成的文件名,同时,多个动静导入的模块如果正文的名字雷同,那么会被打包到一个文件中。/* webpackChunkName: 'components' 
*/'./posts/posts').then(({default: posts}) => {mainElement.appendChild(posts())
    })

MiniCssExtractPlugin

目前状况下,css 款式都是蕴含在 html 的 style 标签中,通过 MiniCssExtractPlugin 插件能够将 css 提取到单个文件中,然而并不是每个我的项目中的 css 都是须要独自提取的,如果提取后的文件中 css 款式较少,那么会导致我的项目申请过多。
增加配置:

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', 
          // 配合 MiniCssExtractPlugin 应用
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [new MiniCssExtractPlugin()
  ]
}

OptimizeCssAssetsWebpackPlugin

默认状况下,webpack 只针对 js 文件进行压缩,如果须要对 css 文件进行压缩,那么须要应用 OptimizeCssAssetsWebpackPlugin 插件。
增加配置:

const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
 // ....
  plugins: [new OptimizeCssAssetsWebpackPlugin()
  ]
}

下面的配置能够实现 css 压缩,然而 webpack 官网举荐将 OptimizeCssAssetsWebpackPlugin 配置在 opitimization 属性中,这样,在 webpack 打包的时候,如果启用了我的项目优化,那么就会进行 css 压缩,反之则不会启用,便于对立治理。

const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = {
  optimization: {
    minimizer: [
      // 实现 js 文件压缩
      new TerserWebpackPlugin(),
      // 实现 css 文件压缩
      new OptimizeCssAssetsWebpackPlugin()]
  }
}

文件名 hash

浏览器中运行的前端我的项目避不开的就是缓存,利用缓存能够放慢我的项目的加载速度,然而有的时候缓存会影响我的项目更新,此时为我的项目中的文件增加 hash,因为文件发生变化,打包后的 hash 值不同,也就是浏览器下载文件的地址就不同,避开了缓存的影响。

webpack 中有三种 hash 模式:

  • hash

我的项目级别的 hash,我的项目下所有的打包文件应用同一个 hash 值。

output: {
    // 8 示意生成 hash 值的位数
    filename: '[name]-[hash:8].bundle.js'
}
  • chunkhash

chunk 级别的 hash,我的项目中,属于同一个 chunk 的文件的 hash 值雷同,如 js 文件中导入了 css 文件,那么打包后,对应的 js 文件和 css 文件的 hash 值雷同。

output: {filename: '[name]-[chunkhash:8].bundle.js'
}
  • contenthash

文件级别的 hash,也就是每个文件都有独自的 hash 值。

output: {filename: '[name]-[contenthash:8].bundle.js'
}

正文完
 0