大前端进阶-模块化

概述

模块化是一种解决问题的方案,一个模块就是实现某种特定功能的文件,能够帮助开发者拆分和组织代码。css

js模块化

JavaScript语言在设计之初只是为了完成简单的功能,所以没有模块化的设计。可是随着前端应用的愈来愈庞大,模块化成为了js语言必须解决的问题。html

模块化发展

js的模块化发展大体能够划分为四个阶段:前端

  • 文件划分

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

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

使用模块:node

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

文件划分方式没法管理模块的依赖关系(不是强制定义模块依赖),并且模块内全部变量都挂载在全局对象上,容易污染全局做用域,命名冲突。jquery

  • 命名空间

将文件内全部的变量都添加到一个命名空间下。
编写模块:webpack

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

使用模块:git

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

使用命名空间的好处是能够尽可能避免命名冲突,可是因为命名空间挂载在全局对象下,依然可以在外部修改模块的变量(没有实现模块私有化)。web

  • 当即执行函数

利用函数做用域,将模块路径包裹在一个当即执行函数中,能够指定须要暴露给外部的变量。
编写模块:shell

;(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")')
image.png
红色框中显示的是代码在哪执行,这个显示很不友好。
再次输入:eval('console.log(123) //# sourceURL=foo.js')
image.png
经过添加sourceURL就能够告诉控制台这个代码是在哪一个文件中执行的。

webpack中经过简单的配置便可实现source-map:
devtool: 'eval',
其中devtool的值是source-map的类型,webpack支持12中source-map类型:
image.png
一般状况下,打包速度快的,调试效果通常都很差,调试效果好的,通常打包速度比较慢,在项目中具体使用哪一种类型,须要本身去斟酌。

  • 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'
}

rollup

rollup也是一种打包工具,相比webpack来讲,rollup更加小巧,是专一于ESM各项特性的高效打包器。

在使用rollup以前,须要安装依赖: npm install --save-dev rollup

快速使用

在项目目录下建立三个测试文件:

  • a.js
import { log } from './b.js'
import foo from './c.js'

// 使用
log(foo)
  • b.js
export function log(msg) {
    console.log(msg)
}

export function error(msg) {
    console.error(msg)
}
  • c.js
export default 'foo'

在命令行中执行: npx rollup ./a.js --format iife --file dist/bundle.js就能够完成快速打包。

--format 指定打包输出格式,iife表示自执行函数。
--file 指定输出文件目录。

打包后的结果以下:

(function () {
    'use strict';

    function log(msg) {
        console.log(msg);
    }

    var foo = 'foo';

    // 使用
    log(foo);

}());

能够看到,相比于webpack,rollup打包后代码更加简洁,并且其默认执行了Tree-shaking,去除了未使用的代码。

配置文件

rollup也支持使用配置文件,只不过即便在项目下添加了rollup.config.js文件,在使用的时候依然要用--config参数指定配置文件路径。

export default {
    input: 'a.js',
    output: {
        file: 'dist/bundle.js',
        format: 'iife'
    }
}

插件

插件是rollup的惟一扩展方式。

rollup-plugin-json

能够利用此插件在打包的时候读取项目目录下的json文件。

安装依赖: npm install --save-dev rollup-plugin-json

在配置文件中添加该插件:

import json from 'rollup-plugin-json'
export default {
    input: 'a.js',
    output: {
        file: 'dist/bundle.js',
        format: 'iife'
    },
    plugins: [
        json()
    ]
}

此时,咱们修改a.js文件,让其读取跟目录下的package.json文件。

import { name, version } from './package.json'
console.log(name, version)

打包后:

(function () {
  'use strict';

  var name = "rollup-test";
  var version = "1.0.0";

  console.log(name, version);

}());

会发现,rollup将json中的相应数据赋值给变量,而后在代码中就可使用该变量了。

rollup-plugin-node-resolve

rollup并不能像webpack那样直接在项目中导入node_modules中的node模块,须要使用rollup-plugin-node-resolve插件。

安装依赖: npm install --save-dev rollup-plugin-node-resolve

import json from 'rollup-plugin-json'
import reslove from 'rollup-plugin-node-resolve'
export default {
    input: 'a.js',
    output: {
        file: 'dist/bundle.js',
        format: 'iife'
    },
    plugins: [
        json(),
        reslove()
    ]
}

添加完上述配置后,就能够像webpack那样经过import方式引入node模块,须要注意的是此插件仅支持引入符合ESM规范的模块。

rollup-plugin-commonjs

因为rollup在设计时就是专门打包ESM规范的文件,所以其须要配合rollup=plugin-commonjs插件来导入符合commonjs规范的模块。

安装依赖: npm install --save-dev rollup=plugin-commonjs

添加配置:

import json from 'rollup-plugin-json'
import reslove from 'rollup-plugin-node-resolve'
import commonjs from 'rollup=plugin-commonjs'
export default {
    input: 'a.js',
    output: {
        file: 'dist/bundle.js',
        format: 'iife'
    },
    plugins: [
        json(),
        reslove(),
        commonjs()
    ]
}

其余

代码拆分

rollup也支持动态导入,动态导入的模块会自动拆分为一个单独的文件。

多入口打包

将配置文件的input属性改成一个数组,也可让rollup支持多入口打包。

须要注意的是,若是使用动态导入或者多入口打包,由于最终打包结果会是多个文件,此时输出的格式就不能再使用iife,可使用amd。

优缺点

rollup相比webpack有着其自然的优缺点。

优势:

  1. 输出结果更加扁平
  2. 自动移除未引用代码
  3. 打包结果依然彻底可读

缺点:

  1. 加载非ESM模块比较复杂
  2. 模块最终都被打包到一个函数中,没法实现hrm
  3. 浏览器环境中,代码拆分依赖amd库(requirejs)

所以在开发一个应用程序的时候,能够选用webpack,当开发一个框架或者类库的时候,可使用rollup(vue中就有rollup的影子)。

Parcel

相比于webpack和rollup,Parcel是最近才出现的打包工具,它的初衷是经过最少的配置完成前端打包,和它造成鲜明对比的是webpack的配置量比较大,须要开发人员掌握的东西比较多。不过随着webpack的流行,人们已经熟悉了其配置,所以Parcel的位置有点尴尬。

不过其也有能够借鉴的地方,如自动安装模块等功能。

规范化

随着前端项目的体量愈来愈大,对开发人员的要求也愈来愈高。尤为针对团队协做的项目,规范化要求愈来愈高。

目前,项目的规范化不只体如今代码上,也体如今文档编写和提交日志等方方面面。

ESLint

eslint是目前最经常使用的代码规范检查插件,其不只能够检查代码规范,同时也能够检查代码中的语法错误。

eslint使用很是方便,经过npm install --save-dev eslint 安装相应模块,经过npx eslint --init安装配置文件,而后就可使用。

git hooks

虽然项目开发时,能够约定你们在提交代码前须要lint一下代码,可是这样没法避免未lint代码的提交,此时就可使用git hooks。

git hooks指的是git钩子,每个钩子对应git的一个命令,命令执行的时候,会触发相应的钩子执行,经过将lint添加到git钩子中,就能够保证代码入库以前经历过lint检查。

钩子体验

每个git项目中都有一个.git文件夹,在其内部hooks文件夹下默认放置了全部的钩子脚本(shell 脚本)。

image.png

找到其中的pre-commit开头的文件,其内部的脚本会在git commit以前执行,手动去掉.sample后缀,再其内部添加一段简单的脚本:

#!/bin/sh
echo "pre commit"

此时在命令行中执行commit命令,就会发现咱们添加的脚本被执行了。

image.png

Husky

并非全部人都能熟练的写脚本命令,所以能够经过Husky模块,在项目中使用这些git hooks。

安装依赖: npm install --save-dev husky。

在package.json中添加Husky属性:

"Husky": {
   "hooks": {
      "pre-commit": "npm run lint"
   }
 }

上述配置的意思就是说,在pre commit钩子执行的时候执行npm run lint脚本,这样就更加方便项目使用。

lint-staged

虽然husky解决了编写脚本的问题,可是husky内部只能为每一个钩子注册一件事,若是咱们想在提交以前利用Prettier自动格式化文件,elint检查文件,而后git add提交文件,那么就可使用lint-staged。

安装依赖:npm install --save-dev lint-staged

修改package.json中的配置:

"scripts": {
    "precommit": "lint-staged"
  },
  "Husky": {
    "hooks": {
      "pre-commit": "npm run precommit"
    }
  },
  "lint-staged": {
    "*.js": [
      "prettier --write",
      "eslint --fix",
      "git add"
    ]
  }

这样配置的化,就能够在commit提交以前,首先触发pre-commit钩子,该钩子会执行npm run precommit,执行precommit的时候会找到lint-staged下匹配该文件的配置,而后依次执行prettier,eslint和git add,方便使用。

StyleLint

stylelint和eslint在用法上类似,只不过其是检查css代码。

Prettier

prettier是一种前端代码格式化工具,能够经过它一键格式化项目中的js,css,sass,vue,jsx,json等文件。

相关文章
相关标签/搜索