大前端学习--模块化开发 学习笔记

模块化开发 学习笔记

文章内容输出来源:拉勾教育大前端高薪训练营

模块化开发

模块化开发时当前最重要的前端开发范式之一 模块化只是思想

一、模块化演变过程

Stage1 文件划分方式

  • 污染全局作用域
  • 命名冲突问题
  • 无法管理模块依赖
  • 早起模块化完全依靠约定

Stage2 命名空间方式

  • 每个模块只暴露一个全局对象,所有模块都挂载到这个对象上
  • 减少了命名冲突的可能
  • 但是没有私有空间,模块成员可以在外部被访问或修改
  • 模块之间的依赖关系没有得到解决

Stage3 IIFE 立即执行函数

  • 使用立即执行函数包裹代码,要输出的遍历挂载到一个全局对象上
  • 变量拥有了私有空间,只有通过闭包修改和访问变量
  • 参数作为依赖声明去使用,使得每个模块的依赖关系变得明显

二、模块化规范

1. CommonJS规范

  • 一个文件就是一个模块
  • 每个模块都有单独的作用域
  • 通过module.exports导出成员
  • 通过require函数载入模块
  • CommonJS是以同步模式加载模块

2. AMD(Asynchronous Module Definition)异步模块规范

  • 模块加载器:Require.js

    // 定义一个模块
    define('module1', ['jquery', './module2'], function ($, module2) {
      return {
        start: function () {
          $('body').animate({ margin: '200px' })
          module2()
        }
      }
    })
    
    // 载入一个模块
    require(['./module1'], function (module1) {
      module1.start()
    })
    
  • 目前绝大多数第三方库都支持AMD规范

  • AMD使用起来相对复杂

  • 模块JS文件请求频繁

3. 淘宝推出的Sea.js + CMD(Common Module Definition)通用模块规范

// CMD 规范 (类似 CommonJS 规范)
define(function (require, exports, module) {
  // 通过 require 引入依赖
  var $ = require('jquery')
  // 通过 exports 或者 module.exports 对外暴露成员
  module.exports = function () {
    console.log('module 2~')
    $('body').append('<p>module2</p>')
  }
})

4. ES Module

// ./modulejs
const foo = 'es modules'
export { foo }
// ./app.js
import { foo } from './module.js'
console.log(foo) // => es modules
  • 自动采用严格模式,忽略’use strict’
  • 每个ESM模块都是单独的私有作用域
  • ESM是通过CORS去请求外部JS模块的
  • ESM的script标签会延迟脚本执行

5. 模块化标准规范

  • 在node.js中使用CommonJS

    CommonJS是node.js内置的模块化工具,只需要遵循CommonJS的标准即可,不需要引入别的依赖

  • 在浏览器中使用ES Modules

    ES Modules是ECMAScript2015

三、常用的模块化打包工具

模块化打包工具的由来:ES Modules存在环境兼容问题、模块文件过多,网络请求频繁,而且所有的前端资源都需要模块化。

打包工具解决的是前端整体的模块化,并不单指JavaScript模块化

1. webpack

webpack:模块打包器、模块加载器、代码拆分、载入资源模块

(1) webpack的基本使用
  • 先在项目的根目录下执行yarn init -y,创建package.json
  • 安装webpack相关依赖:yarn add webpack webpack-cli --dev
  • 查看webpack版本:yarn webpack --version, 4.43.0
  • 执行yarn webpack进行打包,生成了dist目录,里面有main.js文件
  • 修改index.html中的index.js的路径为dist/main.js,并且去掉script标签的type=module的属性
  • 去package.json的scripts中定义一个build任务:"build": "webpack",以后执行yarn build进行打包
(2) webpack的配置文件

webpack.config.js文件是运行在nodejs文件下的js文件,我们需要按照CommonJS的方式编写代码。这个文件需要导出一个对象,我们完成对应的配置选项。

webpack.config.js文件内容:

const path = require('path')

module.exports = {
  entry: './src/index.js', // 指定打包入口文件,如果是相对路径,前面的点不能少
  output: {
    filename: 'bundle.js', // 输出文件的名称
    path: path.join(__dirname, 'output'), // 输出路径,为绝对路径
  }
}
(3) webpack工作模式

直接执行webpack打包的时候,控制台会有警告:

WARNING in configuration
The ‘mode’ option has not been set, webpack will fallback to ‘production’ for this value. Set ‘mode’ option to ‘development’ or ‘production’ to enable defaults for each environment.
You can also set it to ‘none’ to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

说没有指定工作模式,默认以生产模式打包,会进行代码的压缩。

我们可以通过cli命令指定工作模式,就是增加一个--mode的参数,属性有三种选择,production、development、none

  • production:生产模式会默认启动优化,优化我们的打包结果
  • development:开发模式,会自动优化打包的速度,添加一些调试过程中的辅助到代码中
  • none:原始状态的打包,不会做任何处理

可以通过yarn webpack --mode development来执行.

此外,还可以在webpack的配置文件中指定工作模式,也就是增加一个mode属性,例如:mode: "development"

(4) webpack资源模块加载

将配置文件中的entry属性的值改为./src/main.css,然后执行打包命令yarn webpack,会报错:

ERROR in ./src/main.css 1:5
Module parse failed: Unexpected token (1:5)
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

body {
| margin: 0 auto;
| padding: 0 20px;
error Command failed with exit code 2.

因为webpack默认会把文件当做js解析,所以打包css文件时,文件内容不符合JS语法则报错了,报错中提示我们可以寻找正确的loader去解析代码。webpack内部的loader只能解析js,所以我们要手动安装css-loader去处理css代码。

执行命令:yarn add css-loader --dev

然后在webpack的配置文件中增加属性:

module: {
  rules: [
    {
      test: /.css$/,
      use: 'css-loader'
    }
  ]
}

我们增加外部的loader需要在配置文件中增加资源模块module属性,属性值是一个对象,对象中有一个rules数组,数组里每个元素都是一个对象,对象中的test属性是正则式,指明要处理的资源文件,use属性是对该资源进行处理的loader名称。

再次执行打包命令,发现css没有作用,是因为我们使用css-loader只是对css文件进行了打包,但是并没有作用到页面上,接下来还要安装一个style-loader,执行命令:yarn add style-loader --dev

style-loader是将css-loader处理后的结果,通过style的形式追加到页面上

然后将配置文件中的rules对应的处理css资源模块的use属性由'css-loader'改为['style-loader', 'css-loader'],use配置了多个loader,是一个数组,里面的loader从右往左执行,所以要将css-loader写在后面,我们要先用css-loadercss代码转化成js模块,才可以正常打包。

(5) webpack 导入资源模块

虽然webpack的入口文件可以是别的类型文件,但由于前端项目是由JS驱动,所以我们开发时一般将入口文件设置为JS文件,需要用到CSS时,就直接在JS文件中通过import导入即可,如:import './main.css'

webpack建议我们根据代码的需要在JS中动态导入资源文件,因为需要资源的不是应用,而是代码。因为是JavaScript驱动了整个前端应用,这样做的好处是:

  • 逻辑合理,JS确实需要这些资源文件
  • 确保上线资源不缺失,都是必要的
(6) webpack文件资源加载器

安装文件资源加载器:yarn add file-loader --dev,相当于直接拷贝物理文件。不过此时资源文件路径会出现问题,webpack默认认为它打包过后的文件会放在网站的根目录下面,此时需要在配置文件中的output属性中指定publicPath属性值为dist/,即:publicPath: 'dist/',这样在打包时,文件的输出路径前面会拼接上publicPath的值。

(7) webpack URL 加载器

格式:协议 + 媒体类型和编码 + 文件内容

格式: data:[<mediatype>][;base64],<data>

例如:data:text/html;charset=UTF-8,<h1>html content</h1>

  • 先安装url-loader:yarn add url-loader --dev

  • 修改png文件的loader为url-loader

{
  test: /.png$/,
    // use: 'file-loader',
  use: 'url-loader'
}
  • 执行yarn webpack,此时的png文件的URL则为data协议的了。

最佳使用方式:

  • 小文件使用Data URLs,减少请求次数
  • 大文件独立提取存放,提高加载速度

配置方式:

{
  	test: /.png$/,
    // use: 'file-loader',
    use: {
        loader: 'url-loader',
        options: {
          limit: 10 * 1024, // 单位是字节 10KB
        }
    }
}
  • 超过10KB的文件单独提取存放
  • 小于10KB文件转换为Data URLs嵌入代码中

注意:这种方式还是要安装file-loader,因为对超出大小的文件还是会调用file-loader,如果没有file-loader会报错。

(8) webpack 常用加载器分类
  1. 编译转换类,转换为JS代码,如css-loader
  2. 文件操作类,将资源文件拷贝到输出目录,将文件访问路径向外导出,如:file-loader
  3. 代码检查器,统一代码风格,提高代码质量,如:es-loader
(9) webpack 处理ES2015

因为模块带包需要,所以处理importexport,除此之外,并不能转换其他的ES6特性。如果想要处理ES6,需要安装转化ES6的编译型loader,最常用的就是babel-loaderbabel-loader依赖于babel的核心模块,@babel/core@babel/preset-env

  • 执行命令:yarn add babel-loader @babel/core @babel/preset-env --dev
  • 修改js的loader
{
   test: /.js$/,
   use: {
     loader: 'babel-loader',
     options: {
       presets: ['@babel/preset-env']
     }
   },
    exclude: /(node_modules)/, // 这里很重要,千万别忘了,否则会出错的。
 }

注意:

Webpack只是打包工具,

加载器可以用来编译转化代码

(10) webpack的模块加载方式
  1. 遵循ES Modules标准的import声明

  2. 遵循CommonJS标准的require函数。对于ES的默认导出,要通过require('./XXX').default的形式获取

  3. 遵循AMD标准的define函数和require函数

  4. Loader加载的非JavaScript也会触发资源加载

    1. 样式代码中的@import指令和url函数

      @import url(reset.css);
      
      body {
        margin: 0 auto;
        padding: 0 20px;
        max-width: 800px;
        background: url(1.png);
        background-size: cover;
      }
      

      ​ css-loader在处理css代码时,遇到了background属性中的url函数,发现是引入的资源文件是png格式的文件,则将这个资源文件 交给url-loader处理

    2. HTML代码中的图片标签的src属性

      		{
              test: /.html$/,
              use: {
                loader: 'html-loader',
                options: {
                   // html-loader默认只处理页面中的img标签的src属性的资源文件,所以指定其他标签的资源文件也要处理
                  attributes: {
                    list: [
                      {
                        tag: 'img',
                        attribute: 'src',
                        type: 'src'
                      },
                      {
                        tag: 'a',
                        attribute: 'href',
                        type: 'src'
                      }
                    ]
                  }
                }
              }
            }
      
(11) webpack的核心工作原理

在这里插入图片描述

(12) webpack Loader的工作原理

实现一个markdown的文件加载器。

Loader作为webpack的核心机制,内部的工作原理也非常简单,我们通过开发一个自己的loader,来深入了解loader的工作原理。

我们的需求是实现一个markdown文件的加载器,这个加载器可以在代码当中直接导入markdown文件。markdown文件一般是被转换为html过后再去呈现到页面上的,所以我们导入的markdown文件得到的结果就是转换过后的html字符串。

在项目的根目录下新建一个markdown-loader.js文件,每一个webpackloader都需要去导出一个函数,这个函数就是我们这个loader的对我们所加载到的资源的一个处理过程,它的输入就是我们资源文件的内容,输出就是我们此次加工过后的一个结果。

那我们通过source参数去接收输入,然后通过我们的返回值去输出,那这里呢,我们先尝试打印一下这个source,然后直接去返回一个中hello,我们去看一下结果,我们回到webpack配置文件中去添加一个加载新的规则配置,那这里呢,我们匹配到的扩展名就是.md,就是我们刚刚所编写的markdown-loader的模块,那这里呢,我们的use属性不仅仅只可以使用模块的名称,其实对于模块的文件路径也是可以的,那这一点呢其实与node当中的require函数是一样的,所以说我们大家直接使用相对路径去找到这个markdown-loader的,那配置好过后呢,我们运行打包命令,打包过程当中命令他们确实打印出来了我们所导入的markdown的内容,那这也就意味着我们的source确实是所导入的文件内容,但是呢,它同时也爆出一个解析错误,那说的是you many need additional load to handle the result of this loader,就是我们还需要一个额外的加载器来去处理我们当前的加载结果,那这究竟是为什么呢?

module.exports = source => {
  console.log(source)
  return 'hello'
}

其实webpack的加载资源的过程有点类似于一个工作管道,你可以在这个过程当中一次去使用多个loader,但是呢,还要求我们最终这个管道工作过后的结果必须是一段JavaScript代码,那因为我们这返回的内容是一个hello,那它不是一个标准的JavaScript代码,所以我们这才会出现这样的错误提示,那知道这个错误的原因过后呢,解决的办法其实也就很明显了,那要么就是我们这个loader的直接去返回一段标准的JavaScript代码,要么就是我们再去找一个合适的加载器,接着去处理我们这里返回的结果。

那这里呢,我们先来尝试第1种办法回到我们markdown-loader.js的当中,那这里我们将返回的这个内容修改为'console.log("hello")',那这就是一段标准的JavaScript代码,然后呢,我们再一次运行打包,那此时打包过程当中就不再会报错了。

module.exports = source => {
  console.log(source)
  return 'console.log("hello")'
}

那这里呢,我们一起来看一下打包过后的结果究竟是什么样的,我们打开bundle.js当中,然后呢,我们找到最后一个模块,那这里呢其实也非常简单,webpack打包的时候就是把我们刚刚loader加载过后的结果也就是返回的那个字符串直接拼接到我们这个模块当中了,那这也就解释了刚刚为什么说loader的管道最后必须要去返回JavaScript代码的原因,因为如果说你随便去返回一个内容的话,那放到这里语法就有可能不通过。

那知道了这些过后,我们再回到markdown-loader.js的当中,然后接着去完成我们刚刚的需求,那这里呢,我们先去安装一个markdown解析的模块叫做marked,安装命令为:yarn add marked --dev

安装完成过后,我们再回到代码当中去导入这个模块。然后在我们的加载器当中去使用这个模块,去解析来自参数当中的这个source,我们的返回值就是一段html字符串,也就是转换过后的结果,那这儿如果说我们直接去返回这个html的话,那就会面临刚刚同样的问题,那正确的做法呢,就是把这段html变成一段JavaScript代码,那这里呢,其实我们希望是把这一段html作为我们当前这个模块导出的字符串,也就是我们希望通过export导出这样一个字符串,但是如果说我们只是简单的拼接的话,那我们html当中存在的换行符还有它内部的一些引号,拼接到一起就有可能造成语法上的错误,所以说这里我使用一个小技巧,就是通过JSON.stringify先将这个字符串转换为一个标准的JavaScript格式字符串,那此时内部的引号以及换方符都会被转义过来,然后我们再参与拼接,那这样的话就不会有问题了,我们再次运行打包,看一下打包的结果,那此时我们所看到的结果就是我们所需要的了,当然了,除了module.exports这种方式有外,webpack的还允许我们在返回的代码当中直接去使用ESModule的方式去导出.

const marked = require('marked')

module.exports = source => {
  // console.log(source)
  // return 'console.log("hello")'
  const html = marked(source)
  console.log(html)
  // 两种导出方式:
  // return `module.exports=${JSON.stringify(html)}`
  return `export default ${JSON.stringify(html)}`
}

通过第1种方式解决了我们刚刚所看到的那样一个错误,我们再来尝试一下刚刚所说的第2种方法,那就是在我们markdown-loader的当中去返回一个html的字符串,然后我们交给下一个loader处理这个htm的字符串,我们直接去返回marked的解析过后的html,然后我们再去安装一个用于去处理html加载的loader,叫做html-loader,完成过后,我们回到配置文件当中,那这里呢,我们把use属性修改为一个数组,那这样的话我们的loader工作过程当中就会依次去使用多个loader,那不过这里需要注意,就是它的执行顺序是从数组的后面往前面,那也就是说我们应该把先执行的loader放到后面,后执行的loader放到前面。

const marked = require('marked')

module.exports = source => {
  // console.log(source)
  // return 'console.log("hello")'
  const html = marked(source)
  console.log(html)
  return html
}
module: {
    rules: [
      {
        test: /.md$/,
        use: ['html-loader', './markdown-loader.js']
      }
    ]
  }

回到命令行进行打包,此时我们打包的结果仍然是可以的,我们marked处理完的结果是一个html的字符串,然后这个html字符串交给了下一个loader,也就是html-loader,那这个loader又把它转换成了一个导出这个字符串的一个JavaScript代码,那这样的话我们webpack再去打包的时候就可以正常工作了。

那通过以上的这些尝试我们就发现了loader它内部的一个工作原理其实非常简单,就是一个从输入到输出之间的一个转换,那除此之外,我们还了解了loader,它实际上是一种管道的概念,我们可以将我们此次的这个loader的结果交给下一个loader去处理,然后我们通过多个loader去完成一个功能,那例如我们之前所使用的css-loaderstyle-loader之间的一个配合,包括我们后面还会使用到的,像sass或者less这种loader他们也需要去配合我们刚才所说道的这两种loader,这就是我们loader的工作管道这样一个特性。

(13) Webpack插件机制

插件机制的是webpack一个核心特性,目的是为了增强webpack自动化方面的能力。

Loader专注实现资源模块的加载,从而去实现整体项目的打包。

Plugin解决除了资源加载以外的其他的一些自动化工作:

  • 自动在打包之前去清除dist目录

    • 安装:yarn add clean-webpack-plugin --dev

    • webpack.config.js:

      const {CleanWebpackPlugin} = require('clean-webpack-plugin')
      plugins: [
        new CleanWebpackPlugin()
      ]
      
  • 自动生成HTML插件

    • 安装:yarn add html-webpack-plugin --dev

      const HtmlWebpackPlugin = require('html-webpack-plugin')
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin()
      ]
      
    • 自动生成HTML文件到dist目录中,根目录下的index.html则不再需要了

    • HTML中自动注入了bundle.js的引用到HTML文件中

    • 增加配置参数生成HTML文件:

      new HtmlWebpackPlugin({
        title: 'Webpack Plugin Sample',
        meta: {
          viewport: 'width=device-width'
        }
      })
      
    • 通过模板文件生成HTML文件, webpack.config.js中指定HtmlWebpackPlugin的template参数

      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>webpack</title>
      </head>
      <body>
        <div class="container">
          <h1><%= htmlWebpackPlugin.options.title %></h1>
        </div>
      </body>
      </html>
      
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          title: 'Webpack Plugin Sample',
          meta: {
            viewport: 'width=device-width'
          },
          template: './src/index.html'
        })
      ]
      
    • 生成多个HTML页面

       plugins: [
          new CleanWebpackPlugin(),
          // 用于生成index.html 
          new HtmlWebpackPlugin({
            title: 'Webpack Plugin Sample',
            meta: {
              viewport: 'width=device-width'
            },
            template: './src/index.html'
          }),
          // 用于生成about.html 
          new HtmlWebpackPlugin({
            filename: 'about.html'
          })
        ]
      
  • 拷贝那些不需要参与打包的资源文件到输出目录

    • 安装:yarn add copy-webpack-plugin --dev

      const CopyWebpackPlugin = require('copy-webpack-plugin')
      
      new CopyWebpackPlugin({
        patterns: ['public']
      })
      
  • 压缩我们打包结果输出的代码

(14) webpack开发插件

相比于Loader,Plugin拥有更宽的能力范围,Plugin通过钩子机制实现。

Webpack要求插件必须是一个函数或者是一个包含apply方法的对象。通过在生命周期的钩子中挂载函数实现扩展。

class MyPlugin {
  apply (compiler) {
    console.log('MyPlugin 启动')
    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // compilation 可以理解为此次打包的上下文
      for (const name in compilation.assets) {
        // console.log(name) // 文件名
        console.log(compilation.assets[name].source())
        if(name.endsWith('.js')) {
          const contents = compilation.assets[name].source()
          const withoutComments = contents.replace(/\/\*\*+\//g, '')
          compilation.assets[name] = {
            source: () => withoutComments,
            size: () => withoutComments.length
          }
        }
      }
    })
  }
}
plugins: [
  new MyPlugin()
]
(15) webpack开发体验问题
  • 自动进行编译:执行yarn webpack --watch会监视文件的变化自动进行打包
  • 自动刷新浏览器:webpack-dev-server,安装:yarn add webpack-dev-server,执行:yarn webpack-dev-server --open
(16) webpack dev server 静态资源访问

Dev Server默认只会serve打包输出文件,只要是webpack打包输出的文件都会被访问到,其他静态资源也需要被server

devServer: {
    contentBase: './public'
  }

contentBase额外为开发服务器指定查找资源目录

(17) webpack Dev server代理API

webpack-dev-server支持配置代理

  devServer: {
    contentBase: './public',
    proxy: {
      '/api': {// 以/api开头的地址都会被代理到接口当中
        // http://localhost:8080/api/users -> https://api.github.com/api/users
        target: 'https://api.github.com',
        // http://localhost:8080/api/users -> https://api.github.com/users
        pathRewrite: {
          '^/api': ''
        },
        // 不能使用localhost:8080作为请求GitHub的主机名
        changeOrigin: true, // 以实际代理的主机名去请求
      }
    }
  }	
(18) Source Map

运行代码与源代码之间完全不同,如果需要调试应用,错误信息无法定位,调试和报错都是基于运行代码,SourceMap 就是解决这种问题的最好办法。

Source Map解决了源代码与运行代码不一致所产生的问题.

Webpack 支持sourceMap 12种不同的方式,每种方式的效率和效果各不相同。效果最好的速度最慢,速度最快的效果最差

eval函数可以运行字符串当中的js代码:eval(console.log(123))

当DevTool的值为eval,打包后的报错信息只有源代码文件名称,没有行列信息

每个关键词的特点组合:

  • eval- 是否使用eval执行代码模块
  • cheap- Source map是否包含行信息
  • module-是否能够得到Loader处理之前的源代码
  • inline- SourceMap 不是物理文件,而是以URL形式嵌入到代码中
  • hidden- 看不到SourceMap文件,但确实是生成了该文件
  • nosources- 没有源代码,但是有行列信息。为了在生产模式下保护源代码不被暴露

开发模式推荐使用:eval-cheap-module-source-map,因为:

  • 代码每行不会太长,没有列也没问题
  • 代码经过Loader转换后的差异较大
  • 首次打包速度慢无所谓,重新打包相对较快

生产模式推荐使用:none,原因

  • Source Map会暴露源代码
  • 调试是开发阶段的事情
  • 对代码实在没有信心可以使用nosources-source-map
devtool build rebuild production quality
(none) fastest fastest yes bundled code
eval fastest fastest no generated code
eval-cheap-source-map fast faster no transformed code (lines only)
eval-cheap-module-source-map slow faster no original source (lines only)
eval-source-map slowest fast no original source
eval-nosources-source-map
eval-nosources-cheap-source-map
eval-nosources-cheap-module-source-map
cheap-source-map fast slow yes transformed code (lines only)
cheap-module-source-map slow slower yes original source (lines only)
inline-cheap-source-map fast slow no transformed code (lines only)
inline-cheap-module-source-map slow slower no original source (lines only)
inline-source-map slowest slowest no original source
inline-nosources-source-map
inline-nosources-cheap-source-map
inline-nosources-cheap-module-source-map
source-map slowest slowest yes original source
hidden-source-map slowest slowest yes original source
hidden-nosources-source-map
hidden-nosources-cheap-source-map
hidden-nosources-cheap-module-source-map
hidden-cheap-source-map
hidden-cheap-module-source-map
nosources-source-map slowest slowest yes without source content
nosources-cheap-source-map
nosources-cheap-module-source-map
(19) webpack HMR

HMR(Hot Module Replacement) 模块热替换,应用运行过程中,实时替换某个模块,应用运行状态不受影响。

webpack-dev-server自动刷新导致的页面状态丢失。我们希望在页面不刷新的前提下,模块也可以即使更新。热替换只将修改的模块实时替换至应用中。

HMR是webpack中最强大的功能之一,极大程度的提高了开发者的工作效率。

HMR已经集成在了webpack-dev-server中,运行webpack-dev-server --hot,也可以通过配置文件开启.

Webpack中的HMR并不是对所有文件开箱即用,样式文件支持热更新,脚本文件需要手动处理模块热替换逻辑。而通过脚手架创建的项目内部都集成了HMR方案。

HMR注意事项:

  • 处理HMR的代码报错会导致自动刷新
  • 没启动HMR的情况下,HMR API报错
  • 代码中多了很多与业务无关的代码
(20) webpack生产环境优化

我们在生产环境中,更注重开发效率,而在生产环境中,更注重开发效率。

模式(mode)

webpack建议我们为不同的环境创建不同的配置,两种方案:

  • 配置文件根据环境不同导出不同配置

    const path = require('path')
    const webpack = require('webpack')
    const {CleanWebpackPlugin} = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const CopyWebpackPlugin = require('copy-webpack-plugin')
    
    module.exports = (env, argv) => {
      const config = {
        mode: 'none',
        entry: './src/main.js',
        output: {
          filename: 'bundle.js',
          path: path.join(__dirname, 'dist'),
          // publicPath: 'dist/'
        },
        module: {
          rules: [
            {
              test: /.md$/,
              use: ['html-loader', './markdown-loader.js']
            }
          ]
        },
        plugins: [
          new CleanWebpackPlugin(),
          // 用于生成index.html 
          new HtmlWebpackPlugin({
            title: 'Webpack Plugin Sample',
            meta: {
              viewport: 'width=device-width'
            },
            template: './src/index.html'
          }),
          // 用于生成about.html 
          new HtmlWebpackPlugin({
            filename: 'about.html'
          }),
          // 开发过程最好不要使用这个插件
          // new CopyWebpackPlugin({
          //   patterns: ['public']
          // }),
          // new MyPlugin(),
          new webpack.HotModuleReplacementPlugin()
        ],
        devServer: {
          contentBase: './public',
          proxy: {
            '/api': {// 以/api开头的地址都会被代理到接口当中
              // http://localhost:8080/api/users -> https://api.github.com/api/users
              target: 'https://api.github.com',
              // http://localhost:8080/api/users -> https://api.github.com/users
              pathRewrite: {
                '^/api': ''
              },
              // 不能使用localhost:8080作为请求GitHub的主机名
              changeOrigin: true, // 以实际代理的主机名去请求
            }
          },
          // hot: true
          hotOnly: true, // 如果热替换代码报错了,则不刷新
        },
        devtool: 'eval-cheap-module-source-map'
      }
      
      if (env === 'production') {
        config.mode = 'production'
        config.devtool = false
        config.plugins = [
          ...config.plugins,
          new CleanWebpackPlugin(),
          new CopyWebpackPlugin({
            patterns: ['public']
          })
        ]
      }
      return config
    }
    
  • 一个环境对应一个配置文件

Webpack.common.js

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

module.exports = {
  entry: './src/main.js',
  output: {
    filename: `bundle.js`
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: `index.html`
    })
  ]
}

Webpack.dev.js

const common = require('./webpack.common')
const merge = require('webpack-merge')

module.export = merge(common, {
  mode: 'development',
})

Webpack.prod.js

const common = require('./webpack.common')
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = merge(common, {
  mode: 'production',
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
        patterns: ['public']
    })
  ]
})

Package.json

"scripts": {
    "build": "webpack --config webpack.prod.js"
  },

执行打包命令:yarn build

(21) webpack优化配置
  • DefinePlugin 为代码注入全局成员,这个内置插件默认就会启动,往每个代码中注入一个全局变量process.env.NODE_ENV

    const webpack = require('webpack')
    
    plugins: [
        new HtmlWebpackPlugin({
          filename: `index.html`
        }),
        new webpack.DefinePlugin({
          API_BASE_URL: JSON.stringify('http://api.example.com')
        })
      ]
    
  • Tree-Shaking 摇掉代码中未引用到的代码(dead-code),这个功能在生产模式下自动被开启。Tree-Shaking并不是webpack中的某一个配置选项,而是一组功能搭配使用后的效果。

    optimization: {
        usedExports: true,
        minimize: true
      }
    
  • 合并模块函数 concatenateModules, 又被成为Scope Hoisting,作用域提升

    optimization: {
        usedExports: true,
        minimize: true,
        concatenateModules: true
      }
    
  • 很多资料中说如果使用Babel-Loader,会导致Tree-Shaking失效,因为Tree-Shaking前提是ES Modules,由Webpack打包的代码必须使用ESM,为了转化ES中的新特性,会使用babel处理新特性,就有可能将ESM转化CommonJS,而我们使用的@babel/preset-env这个插件集合就会转化ESM为CommonJS,所以Tree-Shaking会不生效。但是在最新版babel-loader关闭了转换ESM的插件,所以使用babel-loader不会导致Tree-Shaking失效

  • sideEffects 副作用,指的是模块执行时除了导出成员之外所做的事情,sideEffects一般用于npm包标记是否有副作用。如果没有副作用,则没有用到的模块则不会被打包。

    在webpack.config.js中开启这个功能:

    optimization: {
        usedExports: true,
        minimize: true,
        concatenateModules: true,
        sideEffects: true
      }
    

    在package.json里面增加一个属性sideEffects,值为false,表示没有副作用,没有用到的代码则不进行打包。确保你的代码真的没有副作用,否则在webpack打包时就会误删掉有副作用的代码,比如说在原型上添加方法,则是副作用代码;还有CSS代码也属于副作用代码。

    "sideEffects": false
    

    也可以忽略掉有副作用的代码:

    "sizeEffects": [
      "./src/extend.js",
      "*.css"
    ]
    
(22) Code Splitting 代码分包/代码分割

webpack的一个弊端:所有的代码都会被打包到一起,如果应用复杂,bundle会非常大。而并不是每个模块在启动时都是必要的,所以需要分包、按需加载。物极必反,资源太大了不行,太碎了也不行。太大了会影响加载速度;太碎了会导致请求次数过多,因为在目前主流的HTTP1.1有很多缺陷,如同域并行请求限制、每次请求都会有一定的延迟,请求的Header浪费带宽流量。所以模块打包时有必要的。

目前的webpack分包方式有两种:

  • 多入口打包:适用于多页应用程序,一个页面对应一个打包入口,公共部分单独抽取。

    entry: {
      index: './src/index.js',
        album: './src/album.js'
    },
    output: {
        filename: '[name].bundle.js'
    },
    // 每个打包入口形成一个独立的chunk
    plugins: [
        new HtmlWebpackPlugin({
          title: 'Multi Entry',
          template: './src/index.html',
          filename: 'index.html',
          chunks: ['index']
        }),
        new HtmlWebpackPlugin({
          title: 'Nulti Entry',
          template: './src/album.html',
          filename: 'album.html',
          chunks: ['album']
        })
      ],
    // 不同的打包入口肯定会有公共模块,我们需要提取公共模块:
    	optimization: {
        splitChunks: {
          chunks: 'all'
        }
      }
    
  • 动态导入:需要用到某个模块时,再加载这个模块,动态导入的模块会被自动分包。通过动态导入生成的文件只是一个序号,可以使用魔法注释指定分包产生bundle的名称。相同的chunk名会被打包到一起。

    import(/* webpackChunkName: 'posts' */'./post/posts').then({default: posts}) => {
      mainElement.appendChild(posts())
    }
    
(23) MiniCssExtractPlugin可以提取CSS到单个文件

当css代码超过150kb左右才建议使用。

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

module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        // 'style-loader',
        MiniCssExtracPlugin.loader,
        'css-loader'
      ]
    }
  ]
},
(24) OptimizeCssAssetsWebpackPlugin 压缩输出的CSS文件

webpack仅支持对js的压缩,其他文件的压缩需要使用插件。

可以使用 optimize-css-assets-webpack-plugin压缩CSS代码。放到minimizer中,在生产模式下就会自动压缩

optimization: {
  minimizer: [
    new TerseWebpackPlugin(), // 指定了minimizer说明要自定义压缩器,所以要把JS的压缩器指指明,否则无法压缩
    new OptimizeCssAssetWebpackPlugin()
  ]
}
(25)输出文件名hash

生产模式下,文件名使用Hash

  • 项目级别的hash

    output: {
          filename: '[name]-[hash].bundle.js'
      },
    
  • chunk级别的hash

    output: {
          filename: '[name]-[chunkhash].bundle.js'
      },
    
  • 文件级别的hash,:8是指定hash长度 (推荐)

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

2. Rollup

Rollup也是一款ESModule打包器,可以将项目中的细小模块打包成整块代码,使得划分的模块可以更好的运行在浏览器环境或者是Nodejs环境。Rollup与Webpack作用非常类似,不过Rollup更为小巧。webpack结合插件可以完成前端工程化的绝大多数工作,而Rollup仅仅是一款ESM打包器,没有其他功能,例如Rollup中并不支持类似HMR这种高级特性。Rollup并不是要与Webpack全面竞争,而是提供一个充分利用ESM各项特性的高效打包器。

(1) rollup使用

./src/message.js

export default {
  hi: 'Hey Guys, I am jal '
}

./src/logger.js

export const log = msg => {
  console.log('---Info----')
  console.log(msg)
  console.log('-----------')
}

export const error = msg => {
  console.error('---Error-----')
  console.error(mes)
  console.error('-------------')
}

./src/index.js

import {log} from './logger'
import messages from './message'

const msg = messages.hi

log(msg)

安装Rollup:yarn add rollup --dev

运行:yarn rollup ./src/index.js --format iife --file dist/bundle.js

./dist/bundle.js

(function () {
  'use strict';

  const log = msg => {
    console.log('---Info----');
    console.log(msg);
    console.log('-----------');
  };

  var messages = {
    hi: 'Hey Guys, I am jal '
  };

  const msg = messages.hi;

  log(msg);

}());

Rollup默认会开启TreeShaking优化输出结果。

配置文件:rollup.config.js

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

运行:yarn rollup --config , 指定配置文件:yarn rollup --config rollup.config.js

(2) rollup插件

Rollup自身的功能就是对ESM进行合并打包,如果需要更高级的功能,如加载其他类型资源模块,导入CommonJS模块,编译ES新特性,Rollup支持使用插件的方式扩展实现,插件是Rollup唯一的扩展方式。

通过导入json文件学习如何使用Rollup插件。

安装插件rollup-plugin-json, 运行:yarn add rollup-plugin-json --dev

Rollup.config.js

import json from 'rollup-plugin-json'

export default {
  input: './src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'iife'
  },
  plugins: [
    json()
  ]
}

./src/index.js

import {log} from './logger'
import messages from './message'
import {name, version} from '../package.json'
const msg = messages.hi

log(msg)
log(name)
log(version)

./dist/bundle.js

(function () {
  'use strict';

  const log = msg => {
    console.log('---Info----');
    console.log(msg);
    console.log('-----------');
  };

  var messages = {
    hi: 'Hey Guys, I am jal '
  };

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

  const msg = messages.hi;

  log(msg);
  log(name);
  log(version);

}());

json中用到的属性被打包进来了,没用到的属性被TreeShaking移除掉了。

(3) rollup 加载npm依赖

Rollup不能像webpack那样通过模块名称加载npm模块,为了抹平差异,Rollup官方提供了一个插件rollup-plugin-node-resolve,通过这个插件,就可以在代码中使用模块名称导入模块。

安装插件:yarn add rollup-plugin-node-resolve --dev

Rollup.config.js

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

./src/index.js

import _ from 'lodash-es' // lodash模块的ESM版本
import {log} from './logger'
import messages from './message'
import {name, version} from '../package.json'
const msg = messages.hi

log(msg)
log(name)
log(version)
log(_.camelCase('hello world'))
(4) Rollup加载CommonJS模块

安装插件: yarn add rollup-plugin-commonjs

Rollup.config.js

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

./src/index.js

import _ from 'lodash-es' // lodash模块的ESM版本
import {log} from './logger'
import messages from './message'
import {name, version} from '../package.json'
import cjs from './cjs.module'
const msg = messages.hi

log(msg)
log(name)
log(version)
log(_.camelCase('hello world'))

log(cjs)

./dist/bundle.js

// ...

  var cjs_module = {
    foo: 'bor'
  };

// ...

  log(cjs_module);
// ...
(5) Rollup代码差分 : 动态导入

使用动态导入的方式实现模块的按需加载,Rollup内部会自动去处理代码的拆分,也就是分包。

Rollup.config.js

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

./src/index.js

import('./logger').then(({ log }) => {
  log('code splitting~')
})
(6) Rollup多入口打包

rollup.config.js

import json from 'rollup-plugin-json'
import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
export default {
  // input: './src/index.js',
  // input: ['src/index.js', 'src/album.js'], // 多入口打包
  input: { // 这种写法也可以进行多入口打包
    foo: 'src/index.js',
    bar: 'src/album.js'
  },
  output: {
    // file: 'dist/bundle.js',
    // format: 'iife',
    dir: 'dist', // 动态导入时会分包成多文件
    format: 'amd' // 动态导入不支持iife
  },
  plugins: [
    json(),
    resolve(),
    commonjs()
  ]
}

注意此时生成的js文件就要以AMD标准的require方式引入

3. Rollup、Webpack选用原则

(1) Rollup优势
  • 输出结果更加扁平,执行效率更高
  • 自动移除未引用的代码
  • 打包结果依然完全可读
(2) Rollup缺点
  • 加载非ESM的第三方模块比较复杂
  • 模块最终都会被打包到一个函数中,无法实现HMR
  • 浏览器环境中,代码拆分功能依赖AMD库

如果我们正在开发应用程序,需要引入大量的第三方库,代码量又大,需要分包打包,Rollup的作用则会比较欠缺。

如果我们正在开发一个框架或者类库,Rollup的这些优点则非常有必要,缺点则可以忽略。所以大多数知名框架/库都在使用Rollup作为模块打包器。

总结:Webpack大而全,Rollup小而美。

选择标准:

  • 开发应用程序选择Webpack
  • 开发框架/库使用Rollup

4. Parcel 零配置的前端应用打包器

安装Parcel:yarn add parcel-bundler --dev

./src/index.html Parcel中的入口文件是HTML文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Parcel Test</title>
</head>
<body>
  <script src="./main.js"></script>
</body>
</html>

./src/main.js

// import $ from 'jquery' // 自动安装依赖
import foo from './foo'
import './style.css'
import img from './1.png'
foo.bar()

// 动态导入,自动拆分模块
import('jquery').then($=>{
  $(document.body).append('<h1>Hello</h1>')
  $(document.body).append(`<img src="${img}" />`)
})

if(module.hot) {
  module.hot.accept(() => {
    console.log('hmr') // 模块热替换
  })
}

./src/foo.js

export default {
  bar: () => {
    console.log('foo jal111 ..')
  }
}

./src/style.css

body {
  background-color: pink;
}

./src/1.png

执行命令:yarn parcel src/index.html 会自动启动一个http服务,并且监听文件的变化,自动开启了模块热替换功能,依赖文件也是自动安装,整个过程都是零配置。

如何以生产模式进行打包:yarn parcel build src/index.html

对于相同体量的项目进行打包,Parcel会比Webpack快很多,因为在Parcel内部使用的是多进程同时去工作,充分发挥了多核CPU的性能,而Webpack中可以使用happypack插件实现这一点。

Parcel首个版本发布于2017年,当时Webpack使用上过于繁琐。Parcel真正意义上实现了完全零配置,而且Parcel构建速度更快。

而现在大多数项目还是使用Webpack作为打包器,可能是因为Webpack有更好的生态、Webpack越来越好用。

猜你喜欢

转载自blog.csdn.net/jal517486222/article/details/107287751
今日推荐