乐趣区

关于javascript:前端考古系列一个需求引发的前端模块化考古

零、故事的开始

从前有个风行说法是 ” 全国 13 亿人,每人给我一块钱我就是亿万富翁 ”

当初老板感觉这个主见很棒,所以让张三来做个网页不便收钱,界面简略点如下所示就好~

能够看到这里就两个逻辑,点击红色按钮开始打钱,点击蓝色链接触发举报。是不是很简略~

这时老板跟张三说:” 唔使急,最紧要快~ 5 分钟后我要看到这个网页 ”,这时候的张三的情绪毫无稳定,什么软件工程可维护性模块化间接抛之脑后,满脑子只剩下一句 ” 老夫写代码就是一把梭 ”.

一、最后的面条代码

五分钟后张三写完了代码间接 scp 传到了公司服务器某现有全动态前端我的项目的目录下,把链接发给老板后长出了一口气 …

当初的代码大略是这样的:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> 亿万富翁打算 </title>
</head>
<body style="text-align: center;">
    <h1> 帮忙 Nodreame 成为亿万富翁 </h1>
    <div><button id="pay"> 话不多说间接打钱 </button></div>
    <div><a href="#" id="inform"> 举报这个帅逼 </a></div>
    <script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
   <script src="微信领取脚本.js"></script>
    <script> var payElem = document.getElementById('pay')
        var informElem = document.getElementById('inform')
        
        var payParams = {} // 领取参数
        var payLogic = _.debounce(function () {// 1. 确定领取形式(临时只反对微信,且以下领取流程仅供学习应用)
            // 2. 确定金额等领取参数
            // 3. 调起微信领取, 期待回调
            // 4. 依据回调后果进入从新领取流程 or 领取实现感激界面
        }, 200)
        var informLogic = _.debounce(function () {
            // 举报逻辑,PM 说间接稍后弹窗即可无需写性能逻辑
            alert('感谢您的举报,处理结果将在 7~15 个工作日内发送至您的手机')
        }, 400)
        payElem.onclick = payLogic
        informElem.onclick = informLogic </script>
</body>
</html> 

当初的领取逻辑函数 payLogic 看起来仿佛还过得去,然而当初老板不称心了:只反对微信领取显著是不够的,要是有土豪就是想要用支付宝、网银、paypal 甚至比特币打钱怎么办?

没方法,和老板对线不是明智之举,只能肝下来持续一把梭了,而后代码就变成了这样:

<script src="微信领取库.js"></script>
<script src="支付宝领取库.js"></script>
<script src="paypal 领取库.js"></script>
...
<script> var payParams_wx = {} // 微信领取参数
  var payParams_zfb = {} // 支付宝领取参数
  var payParams_pp = {} // Paypal 领取参数
  
  var payLogic = _.debounce(function () {
    // 0. 确定领取形式
    if (微信领取) {
      // 1. 确定金额等领取参数
      // 2. 调起微信领取, 期待回调
      // 3. 依据回调后果进入从新领取流程 or 领取实现感激界面
    } else if (支付宝) {
      // 1. 确定金额等领取参数
      // 2. 调起支付宝领取, 期待回调
      // 3. 依据回调后果进入从新领取流程 or 领取实现感激界面
    } else if (paypal) {
      // 1. 确定金额等领取参数
      // 2. 调起 paypal 领取, 期待回调
      // 3. 依据回调后果进入从新领取流程 or 领取实现感激界面
    } 
    ...
  }, 200) </script> 

随着领取形式的减少,引入的脚本、领取参数、领取逻辑也随之减少. 这个为小需要而生的网页开始变得复杂.

张三想起上次长期接管 ” 祖传屎山 ” 的通宵剖析代码经验,决定为了不便当前对我的项目的保护,当初尝试一下对我的项目进行一些优化.

当下最重要的当然是剖析一下我的项目以后存在的问题:

  • 自私有空间:各领取渠道的领取参数只须要在模块内能够拜访即可;
  • 全局变量净化:不应该将操作接口裸露到全局;
  • 依赖治理:领取逻辑没有显式标记对应的依赖

OK,就从这三个点的优化开始吧~

二、模块化意识的沉睡

因为每个领取渠道都有对应领取参数和领取流程逻辑,所以第一个想到的是能够将不同的逻辑拆分到不同的文件中:

如果是 Java 或者 C# 这个写法的确是能解决 全局变量净化 & 自私有空间 的问题(class 包裹),然而在 JS 中应用下面的分文件写法其实 齐全没有解决任何问题,即便将领取参数、逻辑函数分到不同文件,它们依旧会被裸露到全局变量中.

为了解决 全局变量净化 & 自私有空间 的问题,有前辈提出了原生解法 — 利用立刻执行函数实现 ” 伪模块 ”. 这样内部就无法访问领取参数,所以 自私有空间 问题就此解决, 全局变量净化问题 失去了一部分解决(参数不再裸露,办法仍旧挂载到全局):

另外如果对立刻执行函数传入 依赖 作为参数(例如 lodash),那么 ” 伪模块 ” 就能够 ” 显式 ” 地依赖某个库了:

然而很显著的,这个 ” 显式依赖 ” 并没有真正解决了依赖治理的问题.

以后 index.html 引入 lodash,而 ” 伪模块 ” 文件中无需援用就间接应用了 lodash,那么如果在 index.html 中删除了对 lodash 的援用,那么 ” 伪模块 ” 逻辑的执行必然报错. 故这里的问题在于:未在调用处显式申明依赖项.

为了更好的解决这些问题,张三决定查看到技术社区找找可选计划.

三、理解社区标准

张三到技术论坛看了一圈,理解到了一堆社区计划 CommonJS、AMD、CMD、UMD,决定一一理解一下再做决定.

1. CommonJS 标准

CommonJS 标准是 NodeJS 实现模块化参考的规范,看看其模块定义 & 加载的写法:

NodeJS 中个别通过 module.exports 或者 exports 定义模块,再通过 require 加载模块.

因为其模块加载的设计是 同步 的,这对服务端从内存或者硬盘读取模块并无影响,但对于须要通过网络异步下载模块的浏览器端就不太实用了(在网络加载较慢的状况下,模块加载速度过慢会导致长时间白屏)

为了借鉴 CommonJS 的思维来解决浏览器端的模块化问题,大神们提出了 AMD 和 CMD 这两个 “ 异步加载模块 ” 的浏览器模块标准. 两者别离是 RequireJS 和 SeaJS 在推广过程中对模块定义的规范化产出.

2. AMD 标准

1)概述

  • AMD(Asynchronous Module Definition) 即 异步模块定义,是为了解决浏览器端模块化问题提出的标准.
  • 实现库:require.js
  • 次要 API:模块定义 define & 模块加载 require
  • 特点:推崇依赖 ” 依赖前置 ”

2)实际

张三看完文档和教程后感觉计划可行,于是将领取模块基于 AMD 标准重构了,最新文件目录如下(pay/lib 为第三方提供的领取库):

将本来的 js 逻辑移入 main.js 中,html 中只留下 require.js 的引入(requirejs 会主动实现 main.js 的加载):

<script data-main="./main" src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.min.js"></script> 

而后在 main.js 中配置门路 (不便引入依赖) & 通过 require(依赖数组,回调函数) 的形式写入原先的逻辑,并将领取相干逻辑放入对应的文件并依据 AMD 标准实现定义:

从下面的 define 和 require 能够看出,AMD 标准推崇 ” 依赖前置 ”,也就是定义模块 & 编写逻辑前先申明依赖的模块.

实现后刷新页面,能够看到 wx.js 的回调函数中的打印语句曾经执行,阐明 AMD 标准的 “ 依赖前置 ” 会使依赖提前加载并执行其回调函数:

3)小结

AMD 标准通过 define & require 实现了模块的定义和援用,解决了全局净化和公有性的问题.

同时也以 ” 依赖前置 ” 的模式实现了对模块的显式治理,然而写法是绝对繁琐的.

3. CMD 标准

1)概述

  • CMD(Common Module Definition)即 通用模块定义,和 AMD 的指标类似,不过推崇的理念和模块解决的办法与 AMD 不同.
  • 实现库:sea.js
  • 次要 API:模块定义 define & 模块加载 require & use
  • 特点:推崇依赖就近 & 懒执行

2)写法

sea.js 的写法其实和 require.js 的写法很类似所以就不重写了,官网给出了上面的代码:

define(function(require, exports) {var a = require('./a'); // 获取模块 a 的接口, 就近书写
  a.doSomething();}); 

从下面能够看出 CMD 推崇的写法不同于 AMD 的依赖前置,而是在应用到某模块性能左近才对模块进行 require 援用,称为 ” 依赖就近 ”.

CMD define 办法里的函数被称为 factory,蕴含三个参数 require, exports, modules,用于在 factory 中援用模块和裸露接口.

4. UMD

UMD 全称 Universal Module Definition,见名知义,该标准的指标就是平台通用.

当须要同时反对浏览器端和 NodeJS 端应用的时候个别会选用 UMD 标准打包的代码,例如 Vue:

UMD 标准的代码特点是会通过一些对于 exports 和 define 的判断确定环境,例如 vue.js 中的:

社区计划的提出就是 在 JS 尚未反对模块化的期间解决模块化问题,尽管够用然而还是略显繁琐,这时张三想起上次去某网站下软件导致整个电脑都是大天使之剑起初养成了但凡信官网的习惯,所以决定还是先看看官网模块化计划再做决定.

四、官逼同

TC39 将 Modules 退出到 ECMAScript 2015 中,将文件辨别成 脚本 Scripts & 模块 Modules,应用 import & export 实现模块的导入导出,用起来也比 AMD/CMD 直观,ES Module 成为浏览器和服务器通用的模块解决方案。

张三看了 ES6 文档和大神文章,感觉这个由官网规定的规范必定是当前的大趋势,当前当初学一波顺便在我的项目中练练手必定不亏,于是马上口头了起来.

一切都是那么顺畅美妙,并且所有模块都能胜利加载,只是在浏览器关上 index.html 运行时呈现一点谬误:

这是在 index.html 中的 <script> 应用 type=”module” 导致的,这里的 CORS 策略不反对 file 协定,所以要在本地起一个服务器:

yarn global add http-server
http-server 

应用 http://127.0.0.1:8080 尝试拜访网页:

这个谬误曾经写得很显著了,就是 lodash 文件不是应用 ES Module 的模式裸露导致的. 这里应用 lodash-es 的 CDN 来替换即可:

import * as _ from 'https://cdn.jsdelivr.net/npm/lodash-es@4.17.20/lodash.min.js' 

OK 当初页面曾经可能胜利加载,点击领取按钮也能胜利触发打印如下:

张三伸了个懒腰长出一口气,这波终于用最新的官网形式实现了前端模块化了,连忙和老板邀邀功~

五、前端构建

听到张三被动为我的项目做优化老板频频点头,路过的 Leader 却发现了不对的中央,把他拉到一旁问我的项目的设施兼容性解决是怎么做的

这时候的张三心里咯噔一下,完蛋没思考 ES6 的浏览器兼容性!流下冷汗的同时甚至有点想提桶跑路 …

Leader 帮张三看了一下代码,通知他能够用 Webpack + Babel 解决一下我的项目,即能够用最新语法高兴编码又能够避免低版本浏览器呈现不兼容的问题. 工夫紧急,Leader 就间接通知张三 Webpack 和 Babel 的常识,让他听完之后再本人去试试.

1. webpack

之前的 AMD/CMD 计划都是须要浏览器在执行主逻辑前先下载一个 require.js 或者 sea.js,之后再运行主逻辑代码的,这两种计划对于模块依赖关系的解析都是在运行时做的,这样随着我的项目变大加载速度也会随之变慢,如果没上 HTTP/2.0 的话模块文件过多也会导致加载变慢.

为了解决这个问题,官网设计 ES6 的时候是尽量往 动态化 的方向聚拢的,这样做的益处是如果能在编译期间实现依赖关系的剖析,那么就能够在本地提前做好各种优化,运行时只需间接执行代码即可(了解这个概念能够参考一下 Java、C++ 间接编译出可执行文件,想用时只需运行这个可执行文件即可)

既然是往动态化和预编译的方向靠,那么就须要有工具来辅助实现依赖关系治理和优化这件事,webpack 就是泛滥构建工具中怀才不遇的一个,它专一于打包,并且通过各种 loader 和插件为开发者提供了更多更强的能力.

2. Babel

Babel 是一个 JS 编译器,JS 规范不断更新,每年都有更新更好的语法和能力供开发者应用,然而如果间接依照最新标准给定的形式编码的话,很多版本稍低的浏览器都会呈现兼容性问题,为了解决这个问题就有了 Babel.

Babel 让开发者在编码过程中能够应用更加简洁高效的新语法,在公布打包的时候通过转移来生成兼容性更高的执行代码. 其过程如下:

  • 解析:开发者编写的代码通过解析转化为 ES6+ AST(形象语法树)
  • 转译:ES6+ AST 通过插件转译为兼容性更高的 ES5 AST
  • 生成:基于 ES5 AST 生成兼容性高的最终代码

在理论我的项目中能够联合 webpack + Babel 构建简略我的项目打包,便捷开发的同时也能实现较好的兼容性反对.

3. webpack 打包实战

张三听完 Leader 的话感觉本人又行了,马上开始边查资料边实际~

首先是 webpack 的装置和现有代码的略微调整:

yarn init -y
yarn add -D webpack webpack-cli html-webpack-plugin clean-webpack-plugin
yarn add lodash 

接下来对现有我的项目做一些微调:

  • index.html 删除援用 main.js 的 script 标签
  • js 文件中的 lodash 援用改为 import _ from 'lodash' 即可(webpack 具备将 CommonJS 模块编译成 ES Module 的能力)
  • 编写 webpack.config.js 文件如下:

    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const {CleanWebpackPlugin} = require('clean-webpack-plugin');
      
      
    module.exports = {
      mode: 'production',
      entry: './main.js',
      output: {path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
      },
      plugins: [new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({template: 'index.html'}),
      ],
    }; 

OK,至此我的项目微调曾经实现,应用 npx webpack 即可实现打包.

当初能够间接关上编译的 index.html 文件预览页面了,通过测试 ” 领取 ” 性能也能失常应用~

4. webpack + Babel7.x 实战

下面应用 webpack 实现了我的项目打包,接下来应用 Babel 须要先装置一下依赖:

yarn add -D babel-loader @babel/core @babel/preset-env @babel/plugin-transform-arrow-functions
yarn add @babel/polyfill 

这里一一解释下依赖:

  • babel-loader:js 代码的预处理器,用 webpack+babel 打包必备.
  • @babel/core:babel 外围库,必备.
  • @babel/preset-env:在.babelrc 中接管配置 target 和 useBuiltIns,用于确定指标浏览器版本和 polyfill 的需要.
  • @babel/polyfill:用于指标环境中增加缺失的个性(尽管 7.4.0 之后不举荐应用然而为不引入过多概念故临时应用)

接下来在 webpack.config.js 中配置上 babel-loader 用以解决 JS 文件:

而后在根目录创立 .babelrc 文件寄存 babel 的配置:

{
    "presets": [
        [
            "@babel/env",
            {
                "targets": { // 指标平台
                    "edge": "17",
                    "firefox": "60",
                    "chrome": "67",
                    "safari": "11.1",
                },
                // usage 示意依据 target 主动确定须要 polyfill 的性能
                "useBuiltIns": "usage",
            }
        ]
    ],
    "plugins": ["@babel/plugin-transform-arrow-functions",],
} 

为了不便测试是否 Babel 是否真的失效,在 main.js 开端中退出两行代码不便比照打包后果:

[1, 2, 3].map((n) => n + 1);
Promise.resolve().finally(); 

最初用 npx webpack 命令打包即可,查看后果如下:

OK,刚刚增加的箭头函数曾经转换为一般函数,finally 也能搜寻到两个后果,没展现进去的第一个 finnally 应该就是其对应的 polyfill 了.

这样通过这样的终于粗略解决了兼容性问题,张三再次长出了一口气 …

Ending

问题搞定!老板示意十分满意,口头褒扬了张三一番,张三也开始空想起本人升职加薪的样子 …

夜深了,就像张三对前端模块化的理解一样更深了 …

欢送拍砖,感觉还行也欢送点赞珍藏~ 新开公号:「无梦的冒险谭」欢送关注(搜寻 Nodreame 也能够~)旅程正在持续 ✿✿ヽ (°▽°) ノ✿

退出移动版