从前端模块化编程切入想聊聊前端的未来(文末有我想问的问题)

本文深入讲解JavaScript模块化的发展,从全局函数模式到ES6模块化,涵盖了CommonJS、AMD、CMD等规范,分析了每种模式的特点及应用场景,强调了模块化在前端开发中的重要性和工程化价值。

1. JavaScript模块化简述?

1.1 为什么需要模块化

  1. 没有模块化前的项目,常常在一个JS文件中会有很多功能的代码,这使得文件很大,分类性不强,自然而然不易维护;
  2. 那么我们将一个大的JS文件根据一定的规范拆分成几个小的文件的话将会便于管理,可以提高复用性,随之,可以起到分治的效果;
  3. 一个复杂的项目肯定有很多相似的功能模块,如果每次都需要重新编写模块肯定既费时又耗力。同样,某个功能别人已经造好了轮子,我们就调来用用就好,这时就要引用别人编写模块,引用的前提是要有统一的「打开姿势」,如果每个人有各自的写法,那么肯定会乱套,所以会引出模块化规范;
  4. 现在常用的JavaScript模块化规范有四种: CommonjsAMD , CMD , ES6模块化 。个人理解,ES6模块化才是主流。

1.2 模块的定义

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
  • 块的内部数据相对而言是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

所以,我们发现学习或建立模块就是抓住两点:如何引入模块?如何暴露模块?

1.3 模块化的定义

编码时是按照模块一个一个编码的, 整个项目就是一个模块化的项目

1.4 模块化的优势

  • 方便维护代码,更好的分离,按需加载
  • 提高代码复用性
  • 降低代码耦合度(降偶)
  • 分治思想——模块化不仅仅只是复用,不管你将来是否要复用某段代码,你都有充分的理由将其分治为一个模块。(我们在开发中有时候经常会出现一个模块,实则只用到了一次,但还是抽离出来作为单个独立的模块,这就是分而治之的软件工程的思想,在前端模块化同样适用)

2. 模块化的进化史?

2.1 全局Function模式

module1.js (定义一个模块1)

//数据
let data1 = 'module one data'

//操作数据的函数
function foo() {
  console.log(`foo() ${data1}`)
}
function bar() {
  console.log(`bar() ${data1}`)
}
复制代码

module2.js (定义一个模块2)

let data2 = 'module two data';

function foo() {  //与模块1中的函数冲突了
  console.log(`foo() ${data2}`)
}
复制代码

test.html (去使用定义好的模块1和模块2)

//同步引入,若函数冲突,则后面覆盖前面
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript">
  foo()   //foo() module two data
  bar()   //bar() module one data
</script>
复制代码

说明:

  • 全局函数模式: 将不同的功能封装成不同的全局函数
  • 问题: Global被污染了, 很容易引起命名冲突(比如模块中的data1 data2都是全局变量)

2.2 namespace模式

module1.js (定义一个模块1)

let moduleOne = {
  data: 'module one data',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}
复制代码

module2.js (定义一个模块2)

let moduleTwo = {
  data: 'module two data',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}
复制代码

test.html (去使用定义好的模块1和模块2)

<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript">
  moduleOne.foo()   //foo() module one data
  moduleOne.bar()   //bar() module one data

  moduleTwo.foo()  //foo() module two data
  moduleTwo.bar()  //bar() module two data

  moduleOne.data = 'update data' //能直接修改模块内部的数据
  moduleOne.foo()  //foo() update data
</script>
复制代码

说明:

  • namespace模式: 简单对象封装
  • 作用: 减少了全局变量 (如两个模块的 data 都不是全局变量了,而是对象的某一个属性 )
  • 问题: 不安全,可以直接修改模块内部的数据

2.3 IIFE模式

module1.js (定义一个模块1)

(function (window) {
  //数据
  let data = 'IIFE module data'

  //操作数据的函数
  function foo() { //用于暴露的函数
    console.log(`foo() ${data}`)
  }

  function bar() {//用于暴露的函数
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }

  function otherFun() { //内部私有的函数
    console.log('privateFunction go otherFun()')
  }

  //暴露foo函数和bar函数
  window.moduleOne = {foo, bar}
})(window)
复制代码

test.html (去使用定义好的模块1)

<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript">
  moduleOne.foo()  //foo() IIFE module data
  moduleOne.bar()  //bar() IIFE module data    privateFunction go otherFun()
  //moduleOne.otherFun()  //报错,moduleOne.otherFun is not a function
  console.log(moduleOne.data) //undefined 因为我暴露的moduleOne对象中无data
  moduleOne.data = 'xxxx' //不是修改的模块内部的data,而是在moduleOne新增data属性
  moduleOne.foo() //验证内部的data没有改变  还是会输出 foo() IIFE module data
</script>
复制代码

说明:

  • IIFE模式: 匿名函数自调用(闭包)
  • IIFE : immediately-invoked function expression(立即调用函数表达式)
  • 作用: 数据是私有的, 外部只能通过暴露的方法操作
  • 问题: 如果当前这个模块依赖另一个模块怎么办? 见下面IIFE增强版的(模块依赖于jQuery)

2.4 IIFE模式增强

引入jquery到项目中

module1.js (定义一个模块1)

(function (window,$) {
  //数据
  let data = 'IIFE Strong module data'

  //操作数据的函数
  function foo() { //用于暴露的函数
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }

  function bar() {//用于暴露的函数
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }

  function otherFun() { //内部私有的函数
    console.log('privateFunction go otherFun()')
  }

  //暴露foo函数和bar函数
  window.moduleOne = {foo, bar}
})(window,jQuery)
复制代码

test.html (去使用定义好的模块1)

<!--引入的js必须有一定顺序-->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript">
  moduleOne.foo()  //foo() IIFE Strong module data  而且页面背景会变色
</script>
复制代码

说明:

  • IIFE模式增强 : 引入依赖

  • 这就是现代模块实现的基石。其实很像了,有引入和暴露两个方面。

  • 存在的问题:一个页面需要引入多个JS的问题

    <script type="text/javascript" src="module1.js"></script>
    <script type="text/javascript" src="module2.js"></script>
    <script type="text/javascript" src="module3.js"></script>
    <script type="text/javascript" src="module4.js"></script>
    复制代码

    请求过多:一个script标签就是一次请求
    依赖模糊:看不出来谁依赖着谁?哪些模块是有依赖关系的,很难看出来。
    难以维护:内部依赖关系混乱也就难以维护啦

3. 现代模块化方案?

3.1 CommonJS

CommonJS 是服务器端模块的规范,Node.js 就是采用了这个规范。但目前也可用于浏览器端,需要使用 Browserify 进行提前编译打包。

CommonJS 模块化的引入方式使用require ; 暴露的方式使用module.exportsexports

CommonJS基于服务器端

  1. 下载安装node.js

  2. 创建项目结构

    |-modules
      |-module1.js
      |-module2.js
      |-module3.js
    |-index.js
    复制代码
  3. 模块化编码 module1.js (定义一个模块1)
    定义一个没有依赖的模块,此模块用来定义配置常量

    const newsUrl = 'http://localhost:3000/news';
    const commentsUrl = 'http://localhost:3000/comments';
    //通过exports暴露出去
    exports.newsUrl = newsUrl;
    exports.commentsUrl = commentsUrl;
    复制代码

    module2.js(定义一个模块2)
    定义一个有依赖的模块(这个模块2又依赖模块1,故需要引入模块1),用来模拟发送请求获取数据的一个模块

    //引入依赖
    const m1 = require('./module1');
    
    //定义发送请求的方法
    function getNews(url) {
      console.log('发送请求获取数据,请求地址为:' + url);
      return 'newsData';
    }
    function getComments(url) {
      console.log('发送请求获取数据,请求地址为:' + url);
      return 'commentsData';
    }
    
    const newsData = getNews(m1.newsUrl);
    const commentsData = getComments(m1.commentsUrl);
    
    //通过module.exports暴露模块
    module.exports = {
      newsData,
      commentsData
    }
    复制代码

    module3.js(定义一个模块3)
    定义一个模块,用来显示用户数据

    //定义显示内容的方法
    function showData(data) {
      console.log('要显示的信息:' + data);
    }
    //通过module.exports暴露模块
    module.exports = showData;
    复制代码

    index.js (主模块,用来启动整个项目)
    需要引入所有需要启动的模块

    const m2 = require('./modules/module2');
    const showData = require('./modules/module3');
    
    showData(m2.newsData);
    showData(m2.commentsData)
    复制代码

    结果输出:

    发送请求获取数据,请求地址为:http://localhost:3000/news
    发送请求获取数据,请求地址为:http://localhost:3000/comments
    要显示的信息:newsData
    要显示的信息:commentsData
    复制代码
  4. 通过node运行index.js
    执行命令: node index.js

CommonJS基于浏览器端

  1. 创建项目结构

    |-dist    //打包生成文件的目录
    |-src    //源码所在的目录
      |-module1.js
      |-module2.js
      |-module3.js
      |-index.js //应用主源文件(只需打包主模块)
    |-index.html //引入dist里面的打包好的js文件,[需要在html文件中引入就是基于浏览器端咯]
    复制代码
  2. 下载browserify
    全局安装下载: npm install browserify -g

  3. 定义模块代码 module1.js
    定义一个没有依赖的模块,此模块用来定义配置常量

    //定义配置常量
    const newsUrl = 'http://localhost:3000/news';
    const commentsUrl = 'http://localhost:3000/comments';
    //暴露出去
    exports.newsUrl = newsUrl;
    exports.commentsUrl = commentsUrl;
    复制代码

    module2.js
    定义一个有依赖的模块(依赖模块1),用来模拟发送请求获取数据的一个模块

    //引入依赖
    const m1 = require('./module1');
    
    //定义发送请求的方法
    function getNews(url) {
      console.log('发送请求获取数据,请求地址为:' + url);
      return 'newsData';
    }
    function getComments(url) {
      console.log('发送请求获取数据,请求地址为:' + url);
      return 'commentsData';
    }
    
    const newsData = getNews(m1.newsUrl);
    const commentsData = getComments(m1.commentsUrl);
    
    //暴露模块
    module.exports = {
      newsData,
      commentsData
    }
    复制代码

    module3.js
    定义一个模块,用来显示用户数据

    //定义显示内容的方法
    function showData(data) {
      console.log('要显示的信息:' + data);
    }
    //暴露模块
    module.exports = showData;
    复制代码

    index.js (应用的主模块JS)
    主模块,用来启动整个项目。需要引入所有需要启动的模块。

    const m2 = require('./module2');
    const showData = require('./module3');
    
    showData(m2.newsData);
    showData(m2.commentsData);
    复制代码

    打包处理index.js
    执行命令:browserify src/index.js -o dist/bundle.js
    src/index.js 表示就是src目录下的index主模块
    -o 表示 outfile
    dist/bundle.js 表示打包处理结果生成到dist/bundle.js

    在主页面index.html中使用引入:
    直接引入主模块就可以了,因为主模块上就有各种依赖,他会自动去解析打包处理。

    <script type="text/javascript" src="dist/bundle.js"></script>
    复制代码

    结果输出:

    发送请求获取数据,请求地址为:http://localhost:3000/news
    发送请求获取数据,请求地址为:http://localhost:3000/comments
    要显示的信息:newsData
    要显示的信息:commentsData
    复制代码

    如果直接引用未打包处理的index.js 则会报错:

    引入方式:<script src="src/index.js"></script>
    报错信息为:Uncaught ReferenceError: require is not defined-->
    复制代码

    我们现在是基于浏览器端的使用。只有在node环境下才可以直接使用未打包的index.js引入,因为在node环境下有exportsmodularrequire 这些全局方法。node函数中是这样的:function (exports, require, module, filename, dirname) {},所以我们引入一个browserify就会自动配置好这些参数。

彻底说明白module.exportsexports的区别:

nodejs中,module是一个全局变量,类似于在浏览器端的window也是一个全局变量一样的道理。

module.exports 初始的时候置为{}, exports也指向这个空对象。

内部的代码实现是:

var module = {
  id: 'xxxx', // 我总得知道怎么去找到他吧
  exports: {}, // exports 就是个空对象
}

var exports = module.exports;  //exports是对module.exports的引用
//也就是exports现在指向的内存地址和module.exports指向的内存地址是一样的
复制代码

上面的代码可以看出我们平常使用的exports是对module.exports的一个引用,两者都是指向同一个对象。

用一句话来说明就是,模块的require(引入)能看到的只有module.exports这个对象,它是看不到exports对象的,而我们在编写模块时用到的exports对象实际上只是对module.exports的引用。(exports = module.exports)。

我们可以使用exports.a = ‘xxx’exports.b = function () {} 添加方法或属性,本质上它也添加在module.exports所指向的对象身上。但是你不能直接exports = { a: 'xxx'} 这样子的意义就是将exports重新指向新的对象!它和module.exports就不是指向同一个对象,也就这两者已经失去了关系,而nodejsrequire(引入)能看到的是module.exports指向的对象。

故,我们一般都会直接使用 module.exports
再举例说明两者区别:

function foo() {
  console.log('foo');
}

function bar() {
  console.log('bar');
}
复制代码

想要将这两个函数暴露出去,可以直接使用exports

exports.foo = foo;
exports.bar = bar;
复制代码

也可以对module.exports赋值

module.exports = {
  foo: foo,
  bar: bar
}
复制代码

但是不能直接对exports赋值

// 错误
exports = {
  foo: foo,
  bar: bar
}
复制代码

因为这样做仅仅改变了exports的引用,而不改变module.exports。 好,剧终。这个问题讲明白了吧。

总结CommonJS
特点:同步加载,有缓存
用法:(抓住引入和暴露)

  • 暴露模块
    exports
    module.exports
  • 引入模块
    require(路径参数)
    路径: 自定义模块:路径必须以./或者 ../开头
    第三方模块/内置模块/核心模块:路径直接使用模块名称

主要是在服务器端使用的,但是也能在浏览器端运行,需要借助browserify进行编译。

3.2 AMD

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,所以同步加载没有问题。但是如果是浏览器端,同步加载很容易阻塞,这时候AMD规范就出来了。AMD规范则是非同步加载模块,允许指定回调函数。故浏览器端一般会使用AMD规范。

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出 。

  1. 下载require.js, 并引入

    官网: http://www.requirejs.cn/
    github : https://github.com/requirejs/requirejs
    require.js导入项目: js/libs/require.js

  2. 创建项目结构

    |-libs
      |-require.js
    |-modules
      |-alerter.js
      |-dataService.js
    |-main.js
    |-index.html
    复制代码
  3. 定义require.js的模块代码
    dataService.js (定义一个无依赖的模块)

    define(function () {
      let msg = 'hello world lyuya';
      function dataServer() {
        return msg.toUpperCase();
      }
      //暴露这个模块
      return dataServer;
    });
    复制代码

    alerter.js (定义一个有依赖的模块)
    定义方法:define(['模块1', '模块2', '模块3'], function (m1, m2,m3) {}) 注意前后一一对应

    //一定要注意一一对应,前面有,后面一定要有,别忘记后面的传参
    define(['dataServer'],function (dataServer) {
      let msg = dataServer();
      function alerter() {
        alert(msg);
      }
      return alerter;
    });
    复制代码
  4. 应用主(入口):main.js (主模块)

    //配置模块的路径
    requirejs.config({
      baseUrl:'./',  //配置所有引入模块的公共路径(基本路径)
      //模块标识名与模块路径映射
      paths : {
        // 模块名称(一定要与引入的模块名称一一对应): 模块的路径
        dataServer: 'modular/dataServer',  
        //一定不能写文件的后缀名,它会自动补全
        alerter: 'modular/alerter',
        //库/框架自己实现模块化的功能,定义了暴露模块的名称
        jquery: 'libs/jquery-1.10.1'
      }
    })
    
    //主模块,下面requirejs可以用require代替,require是异步可缓存的
    requirejs(['alerter','jquery'],function (alerter,$) {
      alerter();
      $('body').css('background','pink')
    });
    复制代码
  5. 在页面index.html中使用模块

    <!--src引入requirejs模块去用这个模块解析主模块-->
    
    <script data-main="./main" src="./libs/require.js"></script>
    复制代码

总结requireJS
特点:异步加载,有缓存
用法:(抓住引入和暴露)

  • 暴露模块
    在模块内部使用return
  • 定义模块
    define(['模块名'], function (模块暴露内容) {})
    require(['模块名'], function (模块暴露内容) {})
    在模块内部可以使用require定义异步模块
  • 主模块:
    requirejs.config({}) 配置使用的模块路径
    requirejs(['模块名'], function (模块暴露内容) {})
  • html文件引入script标签
    <script data-main='app.js' src='require.js'></script>

AMD(通用模块定义)主要是在浏览器使用的。

3.3 CMD

CMD是根据CommonJS和AMD基础上提出的。
CMD(通用模块定义)和AMD(异步模块定)是比较相似的。
RequireJS 遵循的是 AMD(异步模块定义)规范,SeaJS 遵循的是 CMD (通用模块定义)规范。
seaJS 是国人阿里建立的,代表着海纳百川之意。

  1. 下载sea.js, 并引入

    官网: http://seajs.org/
    github : https://github.com/seajs/seajs
    sea.js导入项目: libs/sea.js

  2. 创建项目结构

    |-libs 
      |-sea.js 
    |-modules 
      |-module1.js 
      |-module2.js 
      |-module3.js 
      |-module4.js 
    |-main.js 
    |-index.html
    复制代码
  3. 定义sea.js的模块代码
    module1.js

    define(function (require, exports, module) {
        /*
          require: 引入依赖模块
          exports: 暴露模块
          module: 暴露模块
         */
        const msg = 'moduleone';
        function getMsg() {
          console.log('module1 getMsg() ' + msg);
          return msg;
        }
        //暴露模块
        module.exports = getMsg;
      })
    复制代码

    module2.js

     define(function (require, exports, module) {
        exports.msg1 = 'lyuya';
        exports.msg2 = 'hello';
      })
    复制代码

    module3.js

      define(function (require, exports, module) {
        //同步引入模块
        const getMsg = require('./module1');
    
        let msg = getMsg();
    
        msg = msg.toUpperCase();
    
        module.exports = {
          msg
        }
    
      })
    复制代码

    module4.js

      //异步引入模块
        require.async('./module2', function (m2) {
          console.log(m2.msg1, m2.msg2);
        })
        console.log('module4执行了~~~');
      })
    复制代码

    main.js :主(入口)模块

    define(function (require) {
        const m3 = require('./module3');
        require('./module4');
    
        console.log(m3.msg);
      })
    复制代码

    index.html:

    <script type="text/javascript" src="libs/sea.js"></script> 
    <script type="text/javascript"> seajs.use('./modules/main') </script>
    复制代码

    结果输出:

     module1 getMsg() moduleone      =====module1.js:12
    
     module4执行了~~~               =====module4.js:9 
    
     MODULEONE                       =====main.js:9
    
     lyuya hello                 =====module4.js:7 
    复制代码

总结seaJS

特点:异步加载,有缓存
用法:

  • 定义模块
    define(function (require, exports, module) {})

  • 引入模块
    同步加载require()
    异步加载require.async(['模块名'], function (模块暴露内容) {})

  • 暴露模块
    exports
    module.exports

  • html文件引入script标签
    <script src='sea.js'></script>
    <script>seajs.use('app.js')</script>

seajsrequirejs一样主要在浏览器中使用。其实这两个一般都很少使用。用的比较多的是commonjs 和马上要介绍的es6模块化

3.4 ES6模块化⭐⭐

ES6模块化的出现,给前端更大的方便。旨在成为浏览器和服务器通用的模块解决方案,但还是主要专门针对浏览器端。其模块功能主要由两个命令构成:exportimport。现在很多项目都在使用ES6模块化规范。

  1. 定义package.json文件

  2. 安装babel-cli, babel-preset-es2015browserify

    npm install babel-cli browserify -g
    npm install babel-preset-es2015 --save-dev
    preset 预设(将es6转换成es5的所有插件打包)

  3. 定义.babelrc文件

    {
     "presets": ["es2015"]
    }
    复制代码
  4. 编码
    module1.js
    分别暴露 后面需要完整的定义(变量或函数定义)

    export function foo() {
       console.log('module1 foo()');
     }
     export function bar() {
       console.log('module1 bar()');
     }
     export const DATA_ARR = [1, 3, 5, 1]
    复制代码

    module2.js
    统一暴露 暴露的是一个对象,要暴露的数据添加为对象的属性/方法

    let data = 'module2 data'
     
     function fun1() {
       console.log('module2 fun1() ' + data);
     }
     
     function fun2() {
       console.log('module2 fun2() ' + data);
     }
     
     export {fun1, fun2}
    复制代码

    module3.js
    静默暴露 只能暴露一个内容,默认暴露的本质:定义了default变量,将后面的值赋值给default变量,暴露出去

    export default {
       name: 'Tom',
       setName: function (name) {
         this.name = name
       }
     }
    复制代码

    app.js 主模块用import引入模块

     import {foo, bar} from './module1'
     import {DATA_ARR} from './module1'
     import {fun1, fun2} from './module2'
     import person from './module3'
    
     import $ from 'jquery'  //引入第三方jQuery模块 npm install jquery@1 --save
    
     $('body').css('background', 'red')
    
     foo()
     bar()
     console.log(DATA_ARR);
     fun1()
     fun2()
    
     person.setName('JACK')
     console.log(person.name);
    复制代码

    输出结果:

     module1 foo()
     module1 bar()
     [1, 3, 5, 1]
     module2 fun1() 
     module2 fun2() 
     JACK
    复制代码
  5. 编译
    使用Babel将ES6编译为ES5代码(但包含CommonJS语法):babel src -d build
    使用Browserify编译js:browserify build/app.js -o dist/bundle.js

  6. 在页面index.html中引入测试
    <script type="text/javascript" src="lib/bundle.js"></script>

总结ES6
特点:动态引入(按需加载),没有缓存
用法:(抓住引入和暴露)

  • 引入模块使用import
    • 对于统一暴露/分别暴露
      import {模块暴露的内容} from '模块路径';import * as m1 from './module1'
      这两者暴露的本质是对象,接收的时候只能以对象的解构赋值的方式来接收值
    • 对于默认暴露
      直接使用import 模块暴露的内容 from '模块路径'
      默认暴露,暴露任意数据类型,暴露什么数据类型,接收什么数据类型
  • 暴露模块使用export
    • 分别暴露 (基本不用)
    • 统一暴露 (暴露多个内容)
    • 默认暴露 (暴露单个内容)

主要是用在浏览器,服务器端也使用。但是现在浏览器和服务器均不支持ES6的模块化语法,所以要借助工具来编译运行

  • babel 将ES6 - ES5 (ES6的模块化语法 编译成commonjs
  • browserifycommonjs语法编译成能让浏览器识别的语法

3. 模块化的扩展阅读

前端模块化开发那点历史
Javascript模块化编程@阮一峰
知乎专栏 | AMD和CMD的区别

4. 本片我想说的话 ? ?

既然说到模块化,其实我更想说说模块化与组件化。这两个概念在前端领域已经十分普遍。

先有模块化后有组件化。组件化是建立在模块化思想上的一次演进,一个变种。所以,我们会在软件工程体系中看过一句话:模块化是组件化的基石。

组件化和模块化的思想都是分而治之的思想。但还是有细小的区分,他们的侧重点有所不同。

组件化更加倾向于UI层面上,是一个可以独立展示内容的「积木」,比如一个页面的头部组件,包含结构HTML、样式CSS、逻辑JS、以及静态资源图片组合一个集合体。一个页面是由众多组件组成的,就像由众多「积木」搭成的「城堡」一样; 模块化更加倾向于功能或者数据的封装,一般由几个组件或1个组件构成的带有一定功能的集合体;

引用一下@张云龙「?大神」对组件化的理解:

就如上图的这个 title组件,包含了结构HTML、样式CSS、逻辑JS、以及静态资源图片,往往组件的组成就是以上四个方面。这个 header文件夹我们可以拿到其他项目中使用,它具有可以独立展示内容的特点。

结合前面提到的模块化开发,整个前端项目可以划分为这么几种开发概念:

那么它们之间的关系如下图所示,一个 应用由多个下图的 页面组成。一个 页面由多个 组件组合。组件中可依赖JS模块。

所以,前端开发现在不仅仅只是别人说的「画画页面实现点效果」的职位,它是实现软件的图形用户界面(Graphical User Interface,简称GUI),是一名软件工程师。现在前端开发都是基于模块化和组件化的开发,可以说算是工程化的项目了。从单页面(SPA)的应用就可以看出JavaScript大大改善了Web应用的用户体验。从谷歌提出PWA(Progressive Web Apps)就可以看出前端在领域的成长。

不仅仅如此,多终端也已经成为时下以及未来的一个必然趋势,移动端、PC端、触摸屏、智能设备、物联网等等,相信前端在跨端的领域下肯定会有更好的解决方案。

但是,如果从整个软件工程来看,我们就会意识到一个惨痛的事实:前端工程师在整个系统工程中的地位太低了。前端是处于系统软件的上游(用户入口),因此没有其他系统会来调取前端系统的服务。而后端它在软件开发中处于下游,后端一方面要为前端提供接口服务,一方面要向中后台以及数据层索取服务,对接层次会更多,地位也就更高了。由此导致,感觉每次需求评估前端往往是最后一道坎,因为上游依托下游,就只能是下游先行了,整体上就会感觉前端对业务的参与度太低了。

甚至,2019了。现在还是有很多团队会把前端开发归类为产品或设计岗位底下,嗯,我不好说什么,唉···。

你在的公司前端的组织架构是肿么样呐??? ??

前端未来一定不会差,就像在人工智能和大数据领域下,不止于前端,前端完全可以融合和细化下去。

引用一位蚂蚁伙伴的话来说:前两年的前端主要矛盾是日益爆发的前端新技术同前端程序猿学不动之间的矛盾,而现在主要矛盾发生了变化,变成了前端日益增长的工程地位诉求同前端工程局限性之间的矛盾。(这人考研政治绝对高分!)

相学长|为前端工程之崛起而编程

在这样新的矛盾下,我们就要化被动为主动,改接受为影响。

好啦,好好学习吧,做一个π型人。打铁还需自身硬。Confidence~?

今天是2019年3月8日,农历二月二(龙抬头),星期五,阴阴天气。我在深圳祝福各位女同胞唷节日快乐永远美丽,祝福男同胞单身幸福~biubiu???????

去泡个澡,今晚早点休息,明天还要去北京大学深圳医院。


此文档作者:吕涯
优快云主页:https://blog.youkuaiyun.com/LY_code
掘金主页:https://juejin.im/user/5b220d93e51d4558e03cb948
若有错误,及时提出,一起学习,共同进步。谢谢。 ???

转载于:https://juejin.im/post/5c82323ce51d453a5f22b281

<think>嗯,用户现在问的是前端模块化的理解。之前他们问过Hash路由History路由的区别,所以可能是在学习前端工程化的内容。模块化确实是前端开发中的重要概念,需要详细解释清楚。 首先,用户可能是刚接触前端,或者已经有一定基础,深入理解模块化。需要从基础讲起,但也要包含足够的技术细节。模块化的历史演变可能是个好的切入点,比如从早期没有模块化问题讲起,说明为什么需要模块化。 然后,用户可能知道模块化的具体实现方式,比如CommonJS、AMD、CMD、ES Module等。需要比较它们的区别适用场景,这样用户能根据项目需求选择合适的方案。 还要考虑用户可能关心模块化带来的好处,比如代码复用、维护性、依赖管理等。同时,需要提到现代前端工具如Webpack、Rollup的作用,因为实际项目中模块化离不开这些工具的支持。 另外,用户可能对模块化与组件化的区别有疑问,或者容易混淆,所以需要简要对比说明。最后,结合当前趋势,比如ES Module的普及原生支持,以及未来发展方向,帮助用户把握最新动态。 需要确保回答结构清晰,分点讲解,使用例子场景说明,避免过于抽象。同时,注意语言通俗易懂,技术术语适当解释,让不同层次的读者都能理解。检查是否有遗漏的重要概念,比如Tree Shaking、模块联邦等高级特性,这些可以增强回答的深度。</think>### 前端模块化深度解析(技术演进 + 核心实践) --- #### 一、模块化本质(哲学层面) $$ \text{模块化} = \text{分治策略} + \text{接口规范} + \text{依赖管理} $$ - **分治**:将复杂系统拆分为 $M = \{m_1, m_2, ..., m_n\}$ - **规范**:定义模块输入输出 $f: (input, config) \rightarrow output$ - **依赖**:建立模块关系图 $G = (V, E)$,其中 $V$ 为模块集合,$E$ 为依赖边 --- #### 二、技术演进史(关键里程碑) | 时期 | 方案 | 数学表达 | 典型问题 | |------------|---------------------|----------------------------|-------------------------| | **原始阶段** | 全局变量 | $window.var = value$ | 命名冲突($P(col) \propto n^2$) | | **命名空间** | IIFE 模式 | $(function(){...})()$ | 依赖管理缺失($\exists \delta \in D$) | | **依赖注入** | RequireJS (AMD) | $define(id, [dep], factory)$ | 异步加载时序问题($T_{async} > T_{sync}$) | | **同步规范** | CommonJS | $module.exports = {...}$ | 浏览器兼容性($C \not\subseteq B$) | | **统一标准** | ES Module (ES6) | $import \alpha \ from \ \beta$ | Tree Shaking 优化($\min \sum |used|$) | --- #### 三、核心特性矩阵 | 维度 | CommonJS | AMD | CMD | ES Module | |----------------|----------------|----------------|----------------|----------------| | **加载方式** | 同步 | 异步 | 延迟执行 | 静态解析 | | **适用环境** | 服务端 | 浏览器 | 浏览器 | 全平台 | | **输出类型** | 动态对象 | 回调函数 | 按需执行 | 静态引用 | | **典型语法** | `require()` | `define()` | `seajs.use()` | `import/export`| | **循环依赖** | 缓存处理 | 复杂回调 | 有限支持 | 静态检测 | | **Tree Shaking**| 不支持 | 不支持 | 不支持 | 原生支持 | --- #### 四、现代工程实践 ##### 1. 模块解析算法 $$ \text{Resolve}(request, parent) \rightarrow path $$ ```javascript // Webpack 增强型解析流程 function enhancedResolve(context, request, callback) { // 1. 文件存在性检测:∃ file ∈ {extensions} // 2. 目录映射检测:∃ alias → path' // 3. moduleFolders 遍历:node_modules 层级搜索 // 4. 分包优化:splitChunks 策略 } ``` ##### 2. 模块生命周期 $$ \begin{aligned} \text{Load} &→ \text{Parse} → \text{Instantiate} \\ &→ \text{Evaluate} → \text{Execute} \end{aligned} $$ - **V8 引擎优化**:预解析跳过未使用模块($\forall m \notin G_{entry}, delay$) ##### 3. 高级模式 - **动态导入**:`import('./module').then(...)`(代码分割点) - **模块联邦**(Webpack 5): ```javascript // 模块提供方 new ModuleFederationPlugin({ name: 'app1', exposes: { './Button': './src/Button.js' } }) // 消费方 import('app1/Button').then(...) ``` --- #### 五、质量评估指标 $$ Q(module) = \alpha \cdot Cohesion + \beta \cdot Coupling + \gamma \cdot Reusability $$ - **内聚性**(Cohesion):模块功能单一性($\uparrow$ 越好) - **耦合度**(Coupling):模块间依赖强度($\downarrow$ 越好) - **可复用性**(Reusability):跨项目适用性($\uparrow$ 越好) --- #### 六、模块化 vs 组件化 | 维度 | 模块化 | 组件化 | |------------|------------------------|------------------------| | **关注点** | 代码组织 | UI 功能单元 | | **粒度** | 文件/功能级 | 可视化元素 | | **依赖** | 代码逻辑依赖 | 数据流/事件通信 | | **复用** | 逻辑复用 | UI + 逻辑复用 | | **典型实现**| ES Module | React/Vue Component | --- #### 七、未来趋势 1. **Bundleless 模式**:基于 ES Module 的 Vite/Snowpack $$ T_{dev} = O(1) \quad (\text{传统打包器 } T_{dev} = O(n)) $$ 2. **WASM 模块**:跨语言模块调用 ```javascript import init from './module.wasm' init().then(instance => { instance._add(2, 3) // C++ 模块 }) ``` 3. **TypeScript 增强**:类型即文档 ```typescript interface ModuleAPI { /** @param {number} input 输入值 */ process(input: number): string } ``` --- ### 最佳实践清单 1. **单一职责原则**:每个模块解决且仅解决一个问题 2. **接口最小化**:export 数量 $\leq 5$(经验值) 3. **依赖清晰化**:避免循环依赖(使用 `dependency-cruiser` 检测) 4. **版本控制**:遵循 SemVer($\text{Major.Minor.Patch}$) 5. **测试隔离**:每个模块配备单元测试(测试覆盖率 $\geq 80\%$) --- ### 经典反模式示例 ```javascript // 1. 上帝模块(3000+ 行代码) import { everything } from 'god-module' // 2. 隐式依赖(假设全局存在 jQuery) function init() { $('#app') // 未声明依赖 } // 3. 模块副作用(自动执行网络请求) export const data = fetch('/api') // 应改为函数调用 ``` --- 通过模块化建设,可实现: $$ \text{系统熵值} \downarrow = \frac{\Delta Chaos}{\Delta Time} \rightarrow 0 $$ 最终达到高可维护性的理状态: $$ \text{维护成本} = O(\log n) \quad \text{而非} \quad O(n^2) $$
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值