webpack与模块化

一、模块化

- CommonJS:

CommonJS 规范就是一套偏向服务端的模块化规范,NodeJS 就采用了这个规范。
一个文件就是模块,拥有独立的作用域
通过 `module.exports` 或 `exports` 对象导出模块内部数据
通过 `require` 函数导入外部模块数据
缺点:基于文件系统,同步加载,不适用于浏览器端
- AMD(Asynchronous Module Definition)
浏览器并没有具体实现该规范的代码,我们可以通过一些第三方库来解决
<script data-main="scripts/main" src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>
通过一个 `define` 方法来定义一个模块,并通过该方法的第二个回调函数参数来产生独立作用域
define(function() {
  // 模块内部代码
})

通过 `return` 导出模块内部数据

通过前置依赖列表导入外部模块数据
`require.js` 也支持 `CommonJS` 风格的语法
define(['require', 'exports', 'module'], function(require, exports, module) {
  class Cart {
    add(item) {
      console.log(`添加商品:${item}`)
    }
  }
  exports.Cart = Cart;
})

- UMD(Universal Module Definition)

严格来说,`UMD` 并不属于一套模块规范,它主要用来处理 `CommonJS`、`AMD`、`CMD` 的差异兼容,是模块代码能在前面不同的模块环境下都能正常运行   
(function (root, factory) {
      if (typeof module === "object" && typeof module.exports === "object") {
        // Node, CommonJS-like
        module.exports = factory(require('jquery'));
    }
    else if (typeof define === "function" && define.amd) {
          // AMD 模块环境下
        define(['jquery'], factory);
    }
}(this, function ($) { // $ 要导入的外部依赖模块
    $('div')
    // ...
    function b(){}
    function c(){}

    // 模块导出数据
    return {
        b: b,
        c: c
    }
}));

- ESM

ESM是ES 模块的缩写。是JS语言为了标准化模块系统的一种方案。

import React from 'react';

export default function() {
    // your function
}

export const function1() {}

总结:

ESM由于具有简单的语法,异步加载的特性,以及Tree-shakeable的特性,因此被广泛使用。
UMD可以在任何环境下使用,并且在ESM不能使用的情况下回选择UMD。
CJS是同步的,适用于后端环境。
AMD是异步的,适用于前端环境。

二、webpack

- 官⽅方⽹网站: https://webpack.js.org/
- 中⽂文⽹网站: https://www.webpackjs.com/
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle
它会从⼊⼝模块出发, 识别出源码中的模块化导⼊语句,递归地找出⼊⼝⽂件的所有依赖,将⼊ ⼝和其所有的依赖打包到⼀个单独的⽂件中。
是⼯程化、⾃动化思想在前端开发中的体现

1、安装

webpack 是一个使用 `Node.js` 实现的一个模块化代码打包工具。所以,我们需要先安装 webpack,安装之前需要搭建好 `Node.js` 环境
注:不推荐全局安装

2、使用

原理就是通过shell脚本在node_modules/.bin⽬录下创建⼀个软链 接。

// package.json
{
    ...,
    "scripts": {
        "start": "webpack"    // scripts 中可以定位到 ./node_modules/.bin/ 目录下
    }
} 

或 

npx webpack
- 把分散的模块文件打包到一个文件中,不需要外部引入了
- 内置了一个小型模块加载器(类似 `requireJS`),实现了打包后的代码隔离与引用
 

3、打包配置

`webpack` 命令在运行的时候,默认会读取运行命令所在的目录下的 `webpack.config.js` 文件,通常我们会在项目的根目录下运行命令和创建配置文件。

我们也可以通过 `—config` 选项来指定配置文件路径:
webpack --config ./configs/my_webpack.config.js

 1Chunk = 1bundle,1个chunk(代码块)可以是多个模块组成的。

数组:webpack会⾃动⽣成另外⼀个⼊⼝模块,并将数组中的每个指定的 模块加载进来,并将最后⼀个模块的module.exports作为⼊⼝模块 的module.exports导出。

const path = require('path')
module.exports = {
  mode: 'production',  // 模式 : `"production" | "development" | "none"`
   /*entry指定打包⼊口⽂文件,有三种不同的形式:`string | object | array`
     默认是entry: {main:  './src/index.js'}
    */ 
  entry: './src/index.js',  
/* output打包后的文件位置:
- 可以指定一个固定的文件名称,如果是多入口多出口(`entry` 为对象),则不能使用单文件出口,需要使用下面的方式
- 通过 `webpack` 内置的变量占位符:`[name]`
*/
  output: {
      path: path.resolve(__dirname, "dist"),  // 输出⽂件到 磁盘的⽬录,必须是绝对路径
     filename: "bundle.js",
     filename: "[name][chunkhash:8].js" // 利用占位符,文件名称不要重复
  }
}
     [name][chunkhash:8]是占位符
    //hash 整个项目的hash值,每构建一次 就会有一个新的hash值
    //chunkhash 根据不同入口entry进行依赖解析,构建对应的chunk,生成相应的hash,
    // 只要组成entry的模块没有内容改动,则对应的hash不变
设置NODE_ENV并不会自动的设置mode

4、loaders

- `loaders`:`webpack` 中灰常核心的内容之一,webpack默认只知道如何处理js和JSON模块, 其他 类型的模块处理就靠它了,不同类型的模块的解析就是依赖不同的 `loader` 来实现的
当 `webpack` 碰到不识别的模块的时候,`webpack` 会在配置的 `module` 中进行该文件解析规则的查找
- `rules` 就是我们为不同类型的文件定义的解析规则对应的 loader,它是一个数组
- 每一种类型规则通过 test 选项来定义,通过正则进行匹配,通常我们会通过正则的方式来匹配文件后缀类型
- `use` 针对匹配到文件类型,调用对应的 `loader` 进行处理
module.exports = {
  ...,
  module: {
      rules: [
      {
        test: /\.(txt|md)$/,
        use: 'raw-loader'   // 处理txt 和 md 这样的非 js 的模块
      },
     {
        test: /\.(png|jpe?g|gif)$/,
        // 把识别出的资源模块,移动到指定的输出⽬目录,并且返回这个资源在输出目录的 
         地址(字符串)
        use: {
          loader: "file-loader",
          options: {
             // placeholder 占位符 [name] 源资源模块的名称
             // [ext] 源资源模块的后缀
            name: "[name]_[hash].[ext]",
            //打包后的存放位置
            outputPath: "./images"
            // 打包后文件的 url
            publicPath: './images',
          }
        }
      },
      {
       test: /\.(png|jpe?g|gif)$/,
// 把图片转成 `base64`  格式的字符串,并打包到 `js` 中,对⼩体积的图片⽐较合适
           use: {
             loader: "url-loader",
             options: {
               // placeholder 占位符 [name] 源资源模块的名称
               // [ext] 源资源模块的后缀
               name: "[name]_[hash].[ext]",
               //打包后的存放位置
               outputPath: "./images"
               // 打包后文件的 url
               publicPath: './images',
               // 小于 100 字节转成 base64 格式
               limit: 100
            }
          }
       },
       {
        test: /\.css$/,
//分析 `css` 模块之间的关系,并合成⼀个 `css`
            use: {
               loader: "css-loader",
               options: {
              // 启用/禁用 url() 处理
              url: true,
              // 启用/禁用 @import 处理
              import: true,
                       // 启用/禁用 Sourcemap
                      sourceMap: false
               }
            }
    },
       // 把 `css-loader` 生成的内容,用 `style` 标签挂载到⻚面的 `head` 中
// 同一个任务的 `loader` 可以同时挂载多个,处理顺序为:从右到左,也就是先通过 `css-loader` 处理,然后把处理后的 `css` 字符串交给 `style-loader` 进行处理
        {
        test: /\.css$/,
               use: [
                  {
                      loader: 'style-loader',
                      options: {
injectType: "singletonStyleTag" // 将所 有的style标签合并成⼀个
} },
"css-loader" ] } ] } }

把 `sass` 语法转换成 `css` ,依赖 `node-sass` 模块 

⼀个loader只处理⼀件事情,loader有顺序,从右到左,从下到上

5、plugins

- `plugins`:`webpack` 中另外一个核心的内容,它主要是扩展 `webpack` 本身的一些功能,它们会运行在各种模块解析完成以后的打包编译阶段,比如对解析后的模块文件进行压缩等
// 打包结束后,⾃动生成⼀个 `html` 文件,并把打包生成的 js 模块引⼊到该 `html` 中
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
    ...
  plugins: [
     new HtmlWebpackPlugin({
       title: "My App",
       filename: "app.html",
       template: "./src/html/index.html"
     }) 
  ]
};

// 在 `html` 模板中,可以通过 `<%=htmlWebpackPlugin.options.XXX%>` 的方式获取配置的值
<title><%=htmlWebpackPlugin.options.title%></title>
更多的配置
- `title`: ⽤来生成⻚面的 `title` 元素
- `filename`: 输出的 `HTML` ⽂件名,默认是 `index.html`, 也可以直接配置子目录
- `template`: 模板⽂件路径,⽀持加载器(`loader`),⽐如 `html!./index.html`
- `inject`: `true | 'head' | 'body' | false`,注⼊所有的资源到特定的 `template` 或者 `templateContent` 中,如果设置为 `true` 或者 `body`,所有的 `javascript` 资源将被放置到 `body` 元素的底部,`'head'` 将放置到 `head` 元素中
- `favicon`: 添加特定的 `favicon` 路径到输出的 `HTML` 文件中
- `minify`: `{} | false`, 传递 `html-minifier` 选项给 `minify` 输出
- `hash`: `true | false`,如果为 `true`,将添加 `webpack` 编译生成的 `hash` 到所有包含的脚本和 `CSS` ⽂件,对于解除 `cache` 很有用
- `cache`: `true | false`,如果为 `true`,这是默认值,仅在文件修改之后才会发布文件
- `showErrors`: `true | false`,如果为 `true`,这是默认值,错误信息会写入到 `HTML` ⻚面中
- `chunks`: 允许只添加某些块 (⽐如,仅 unit test 块)
- `chunksSortMode`: 允许控制块在添加到⻚面之前的排序方式,⽀持的值:`'none' | 'default' |{function}-default:'auto'`
- `excludeChunks`: 允许跳过某些块,(⽐如,跳过单元测试的块)
删除(清理)构建目录
提取 `CSS` 到一个单独的文件中
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
module.exports = {
    ...,
  module: {
      rules: [
          {
              test: /\.s[ac]ss$/,
              use: [
                  {
                      loader: MiniCssExtractPlugin.loader
                    },
          'css-loader',
          'sass-loader'
        ]
            }
      ]
    },
  plugins: [
    ...,
    new CleanWebpackPlugin(),  // 删除(清理)构建目录
    new MiniCssExtractPlugin({
        filename: '[name].css'
    }),
    ...
  ]
}

6、sourceMap

我们实际运行在浏览器的代码是通过 `webpack` 打包合并甚至是压缩混淆过的代码,所生成的代码并不利于我们的调试和错误定位,我们可以通过 `sourceMap` 来解决这个问题,`sourceMap` 本质是一个记录了编译后代码与源代码的映射关系的文件,我们可以通过 `webpack` 的 `devtool` 选项来开启 `sourceMap`
首先,编译后会为每一个编译文件生成一个对应的 `.map` 文件,同时在编译文件中添加一段对应的 `map` 文件引入代码
//# sourceMappingURL=xx.js.map
/*# sourceMappingURL=xx.css.map*/
同时,现代浏览器都能够识别 `sourceMap` 文件,如 `chrome`,会在 `Sources` 面板中显示根据编译文件与对应的 `map` 文件定位到源文件中,有利于我们的调试和错误定位

7、devServer与Proxy

每次的代码修改都需要重新编译打包,刷新浏览器,特别麻烦,我们可以通过安装 `webpackDevServer` 来改善这方面的体验
npx webpack-dev-server  或者,`package.json` 中添加 `scripts`
"scripts": {
  "server": "webpack-dev-server"
}
修改 `webpack.config.js`
module.exports = {
  ...,
  devServer: {
      // 生成的虚拟目录路径
      contentBase: "./dist",
      // 自动开启浏览器
      open: true,
      // 端口
      port: 8081
    }
}
启动服务以后,`webpack` 不在会把打包后的文件生成到硬盘真实目录中了,而是直接存在了内存中(同时虚拟了一个存放目录路径),后期更新编译打包和访问速度大大提升
当下前端的开发都是前后端分离开发的,前端开发过程中代码会运行在一个服务器环境下(如当前的 `WebpackDevServer`),那么在处理一些后端请求的时候通常会出现跨域的问题。`WebpackDevServer` 内置了一个代理服务,通过内置代理就可以把我们的跨域请求转发目标服务器上(`WebpackDevServer` 内置的代理发送的请求属于后端 - `node`,不受同源策略限制),具体如下:
<!--后端代码,以 node 为例-->

const Koa = require('koa');
const KoaRouter = require('koa-router');

const app = new Koa();
const router = new KoaRouter();

router.get('/api/info', async ctx => {
    ctx.body = {
        username: 'zMouse',
        gender: 'male'
    }
})

app.use( router.routes() );
app.listen(8787);
默认情况下,请求接口会出现跨域请求错误,修改 `webpack` 配置
module.exports = {
  ...,
  devServer: {
      // 生成的虚拟目录路径
      contentBase: "./dist",
      // 自动开启浏览器
      open: true,
      // 端口
      port: 8081,
      proxy: {
          '/api': {
              target: 'http://localhost:8787'
           }
        }
    }
}
通过 `proxy` 设置,当我们在当前 `WebpackDevServer` 环境下发送以 `/api` 开头的请求都会被转发到 http://localhost:8787 目标服务器下

8、热更新

当代码有变化,我们使用的 `live reload`,也就是刷新整个页面,虽然这样为我们省掉了很多手动刷新页面的麻烦,但是这样即使只是修改了很小的内容,也会刷新整个页面,无法保持页面操作状态。`HMR` 随之就出现了,它的核心的局部(模块)更新,也就是不刷新页面,只更新变化的部分
module.exports = {
  ...,
  devServer: {
      // 生成的虚拟目录路径
      contentBase: "./dist",
      // 自动开启浏览器
      open: true,
      // 端口
      port: 8081,
      // 开启热更新
      hot:true,
      // 即使 HMR 不生效,也不去刷新整个页面(选择开启)
       hotOnly:true,
}
开启 `HMR` 以后,当代码发生变化,`webpack` 即会进行编译,并通过 `websocket` 通知客户端(浏览器),我们需要监听处理来自 `webpack` 的通知,然后通过 `HMR` 提供的  `API` 来完成我们的局部更新逻辑
if (module.hot) {//如果开启 HMR
    module.hot.accept('./fn1.js', function() {
      // 更新逻辑
      box1.onclick = fn1;
    })
}
上面代码就是 当 ./fn1.js 模块代码发生变化的时候,把最新的 fn1 函数绑定到 box1.onclick 上

从上面就可以看到,`HMR` 其实就是以模块为单位,当模块代码发生修改的时候,通知客户端进行对应的更新,而客户端则根据具体的模块来更新我们的页面逻辑(这些逻辑需要自己去实现),好在当前一些常用的更新逻辑都有了现成的插件

样式热更新比较简单,`style-loader` 中就已经集成实现了,我们只需要在 `use` 中使用就可以了
react 脚手架与vue 脚手架中也有集成
 
 
posted @ 2022-09-14 10:27  菲比月  阅读(110)  评论(0编辑  收藏  举报