概述

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

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.jsexport 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.jsmodule.exports = {    foo: 'foo'}// moduleB.jsconst { 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.mjsexport const foo = 'foo'export default function(){    console.log(foo)}// moduleB.mjsimport { foo } from './moduleA.mjs'console.log(foo)

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

commonjs交互

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

// moduleA.jsmodule.exports = {    foo: 'foo'}// moduleB.mjsimport * 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和dirnameconsole.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 = editormodule.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'}