精华内容
下载资源
问答
  • 前端模块化
    2022-01-08 10:10:06

    前言
    随着前端技术的发展,模块化开发已经是前端开发通用解决方案。

    本文主要介绍了模块化的概念、由来、优点以及前端开发中常见的模块化规范。

    一、认识模块化
    模块概念?
    对于一个复杂的程序,将其按照一定的规范封装成几个文件块,每一块向外暴露一些接口, 但是块的内部数据是私有的, 块与块之间通过暴露的接口进行通信,这个过程称为模块化。

    一个模块具有的基本特征:

    代码封装,避免全局污染
    具有唯一标识
    暴露部分数据或者api方法供外部使用
    模块使用方便快捷
    为什么会有模块化
    模块化的由来,需要从早期的开发模式说起。

    无封装无模块
    早期的js只是作为一个浏览器脚本的身份,来实现一些简单的交互操作。

    由于代码量少,所以直接将代码放在<script>标签中即可。

    <script>
      document.getElementById('hello').onClick = function () {
        alert('hello');
      }
    
      document.getElementById('submit-btn').onClick = function () {
        var username = document.getElementById('username').value;
        var password = document.getElementById('password').value;
        if (!username) {
          alert('请输入用户名');
          return;
        }
        if (!password) {
          alert('请输入密码');
          return;
        }
        // 提交表单
        console.log('提交表单');
      }
    </script>
    复制代码
    

    随着前端技术的发展,js的广泛应用,代码量日益增加。原始的代码堆砌方式会有很多弊端:

    产生大量重复的代码,同样功能的代码到处复制粘贴
    逻辑混乱,不利于阅读和维护
    很容易出现bug。
    于是便将一部分功能封装到函数中,通过调用函数来执行。

    封装全局功能函数
    通过将不同的功能封装成不同的函数,进行调用

    // 张三定义了setValue函数
    function setValue(name){
      document.getElementById('username').value = name;
    }
    // 张三定义了getValue函数
    function getValue(){
      return document.getElementById('username').value;
    }
    
    // 李四定义了setValue函数
    function setValue(name){
      document.getElementById('phone').value = name;
    }
    // 李四定义了getValue函数
    function getValue(){
      return document.getElementById('phone').value;
    }
    复制代码
    

    张三定义了setValue和getValue方法,实现了自己的功能,测试了下没有问题

    第二天李四增加了功能,也定义了setValue和getValue方法,测试了下自己的功能,也没有问题

    第三天,测试给张三提bug了。

    所以,这种方式也有弊端:会污染全局命名空间, 容易引起命名冲突,而且模块成员之间看不出依赖

    命名空间
    声明一个命名空间对象,将数据和方法封装到对象中,减少了全局变量,解决命名冲突

    const tool = {
      id: 'tool_1',
      type: 'input',
      value: '123',
      getType() {
        console.log(`type-${this.type}`);
        return this.type;
      }
      getValue() {
        console.log(`value-${this.value}`);
        return this.value;
      },
    
    }
    tool.type = 'checkbox' // 直接修改模块内部的数据
    tool.getType() // 'checkbox'
    复制代码
    

    这样的写法会暴露所有模块成员,内部状态可以被外部改写,导致数据安全问题。

    立即执行函数、依赖注入
    将数据和方法封装到一个函数内部, 通过给window添加属性来向外暴露api接口

    // tool.js文件
    (function(window, $) {
      let id = '#tool_1';
      let type = 'input';
      let value = '123';
      let count = 0;
    
      // 函数
      function getType() {
        console.log(`type-${this.type}`);
        return type;
      }
      function getValue() {
        console.log(`value-${$(id).val()}`);
        return $(id).val();
      }
      function setValue(val) {
        value = val;
      }
      function increase() {
        count++;
      }
      // 私有方法
      function resetValue() {
        value = '123';
      }
      // 私有方法
      function resetCount() {
        count = 0;
      }
    
      function resetHandler() {
        console.log('resetHandler');
        resetValue();
        resetCount();
      }
    
      // 暴露方法
      window.tool = { getType, getValue, setValue, increase, resetHandler }
    })(window, jQuery)
    复制代码
    

    引入js时必须保证顺序正确 (index.html文件)

    <script type="text/javascript" src="jquery-1.7.2.js"></script>
    <script type="text/javascript" src="tool.js"></script>
    <script type="text/javascript">
      tool.setValue('567');
    </script>
    复制代码
    

    上面例子通过jquery方法获取input框的值,所以必须先引入jQuery库,当作参数传入。

    原始开发方案局限性:
    script标签 + 函数封装 + 命名空间 + 立即执行函数,这些方式有很大的局限性

    1、全局空间污染
    2、需手动管理依赖,不具备可扩展性
    3、重复加载与循环引用的问题
    3、无法实现按需加载
    4、产生大量的http请求
    模块化的优点
    避免命名冲突(减少命名空间污染)
    功能分离, 按需加载
    可复用性
    可维护性
    接下来介绍开发中常用的commonjs, AMD, CMD, ES6规范。

    二、模块化规范
    CommonJS
    CommonJS概述
    一个文件代表一个模块,有自己的作用域。模块中定义的变量、函数、类,都是私有的,通过暴露变量和api方法给外部使用。

    Node采用了CommonJS模块规范,但并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性。

    CommonJS特点
    所有代码都运行在模块作用域,不会污染全局作用域。

    模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。

    模块加载的顺序,按照其在代码中出现的顺序。

    运行时同步加载

    CommonJS基本用法
    模块定义和导出

    // moduleA.js
    let count = 0;
    
    module.exports.increase = () => {
      count++;
    };
    
    module.exports.getValue = () => {
      return count;
    }
    复制代码
    

    模块引入

    require命令用于加载模块文件,如果没有发现指定模块,会报错。

    可以在一个文件中引入模块并导出另一个模块。

    // moduleB.js
    
    // 如果参数字符串以“./”或者“../”开头,则表示加载的是一个相对路径的文件
    const { getValue, increase } = require('./moduleA');
    
    increase();
    let count = getValue();
    console.log(count);
    
    module.exports.add = (val) => {
      return val + count;
    }
    复制代码
    

    模块标识

    模块标识就是require(moduleName)函数的参数moduleName,参数需符合规范:

    必须是字符串
    如果是第三方模块,则moduleName为模块名
    如果是自定义模块,moduleName为模块文件路径; 可以是相对路径或者绝对路径
    可以省略后缀名
    CommonJS模块规范的意义在于将变量和方法等限制在私有的作用域中,每个模块具有独立的空间,它们互不干扰,同时支持导入和导出来衔接上下游模块。

    CommonJS模块的加载机制
    一个模块除了自己的函数作用域之外,最外层还有一个模块作用域,module代表这个模块,是一个对象,exports是module的属性,是对外暴露的接口。

    require也在这个模块的上下文中,用来引入外部模块,其实就是加载其他模块的module.exports属性。

    接下来分析下CommonJS模块的大致加载流程

    function loadModule(filename, module, require, __filename, __dirname) {
      const wrappedSrc = `(function (module, exports, require, __filename, __dirname) { 
        ${fs.readFileSync(filename, "utf8")} // 使用的是fs.readFileSync,同步读取
        })(module, module.exports, require, __filename, __dirname)`;
      eval(wrappedSrc);
    }
    复制代码
    

    这里只是为了概述加载的流程,很多边界及安全问题都不予考虑,如:

    这里我们只是简单的使用 eval来我们的JS代码,实际上这种方式会有很多安全问题,所以真实代码中应该使用 vm来实现。

    源代码中还有额外两个参数: __filename和 __dirname,这就是为什么我们在写代码的时候可以直接使用这两个变量的原因。

    require实现

    function require(moduleName) {
      // 通过require.resolve解析补全模块路径,得到一个绝对路径字符串
      const id = require.resolve(moduleName);
      // 先查询下该id路径是否已经缓存到require.cache中,如果已经缓存过了,则直接读缓存
      if (require.cache[id]) {
        return require.cache[id].exports;
      }
      // module 元数据
      const module = {
        exports: {},
        id,
      };
      // 新加载模块后,将模块路径添加到缓存中,方便后续通过id路径直接读缓存
      require.cache[id] = module;
      // 加载模块
      // loadModule(id, module, require);
      // 直接将上面loadModule方法整合进来
      (function (filename, module, require) {
        (function (module, exports, require) {
          fs.readFileSync(filename, "utf8");
        })(module, module.exports, require);
      })(id, module, require);
    
      // 返回 module.exports 
      return module.exports;
    }
    
    require.cache = {};
    require.resolve = (moduleName) => {
      /* 解析补全模块路径,得到一个绝对路径字符串 */
      return '绝对路径字符串';
    };
    复制代码
    

    上面的模块加载时,将module.exports对象传入内部自执行函数中,模块内部将数据或者方法挂载到module.exports对象上,最后返回这个module.exports对象。

    以前面的moduleA.js和moduleB.js模块为例:

    moduleA 模块中将 increase 和 getValue 方法挂载到 上下文的 module.exports对象上

    // moduleA.js
    let count = 0;
    
    module.exports.increase = () => {
      count++;
    };
    
    module.exports.getValue = () => {
      return count;
    }
    复制代码
    

    moduleB 模块中 require 了 moduleA,并return 挂载了increase 和 getValue方法的module.exports对象;这个对象经过结构赋值,最终被moduleB中的increase 和 getValue变量接收。

    // moduleB.js
    const { getValue, increase } = require('./moduleA');
    
    //等价于
    // let m = require('./moduleA');
    // const getValue = m.getValue;
    // const increase = m.increase;
    
    increase();
    let count = getValue();
    console.log(count);
    
    module.exports.add = (val) => {
      return val + count;
    }
    复制代码
    

    require.resolve加载策略
    在前面我们已经知道了resolve是require的方法属性。它的作用就是把传递进来的路径进行补全得到一个绝对路径的字符串。

    function require(moduleName) {
      ......
      // 返回 module.exports 
      return module.exports;
    }
    require.resolve = (moduleName) => {
      /* 解析补全模块路径,得到一个绝对路径字符串 */
      return '绝对路径字符串';
    };
    复制代码
    

    在实际项目中,我们经常使用的方式有:

    导入自己写的模块文件
    导入nodejs提供的核心模块
    导入node_modules里的包模块
    我们可以简单地概括下加载策略:

    首先判断是否为核心模块,在nodejs自身提供的模块列表中进行查找,如果是就直接返回
    判断参数 moduleName 是否以./或者…/开头,如果是就统一转换成绝对路径进行加载后返回
    如果前两步都没找到,就当做是包模块,去最近的node_moudles目录中查找
    由于moduleName是可以省略后缀名的,所以应该遵循一个后缀名判断规则,不同后缀名判断的优先级顺序如下:

    如果moduleName是带有后缀名的文件,则直接返回;
    如果moduleName是不带后缀名的路径,则按照一下顺序加载

    moduleName.js
    moduleName.json
    moduleName.node
    moduleName/index.js
    moduleName/index.json
    moduleName/index.node
    

    如果是加载的是包模块的话,就会按照包模块中package.json文件的main字段属性的值来加载
    Nodejs的模块化实现
    Nodejs模块在实现中并非完全按照CommonJS来,进行了取舍,增加了一些自身的的特性。

    Nodejs中一个文件是一个模块: module, 一个模块就是一个Module的实例

    Nodejs中Module构造函数:

    function Module(id, parent){
      this.id = id;
      this.exports = {};
      this.parent = parent;
      if(parent && parent.children) {
        parent.children.push(this);
      }
      this.filename = null;
      this.loaded = false;
      this.children = [];
    }
    
    //实例化一个模块
    var module = new Module(filename, parent);
    复制代码
    

    其中id是模块id,exports是这个模块要暴露出来的api接口,parent是父级模块,loaded表示这个模块是否加载完成。

    AMD
    AMD(Asynchronous Module Definition),异步模块定义:主要用于浏览器,由于该规范不是原生js支持的,使用AMD规范进行开发的时候需要引入第三方的库函数,也就是流行的RequireJS RequireJS是AMD规范的一种实现。其实也可以说AMD是RequireJS在推广过程中对模块定义的规范化产出。

    AMD是一个异步模块加载规范,它与CommonJS的主要区别就是异步加载,允许指定回调函数。模块加载过程中即使require的模块还没有获取到,也不会影响后面代码的执行。

    由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑异步加载的方式,所以CommonJS规范会比较适用。但是,浏览器环境,要从服务器端下载模块文件,这时就必须采用异步加载,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要早。

    AMD规范基本语法
    RequireJS定义了一个define函数,用来定义模块

    语法:

    define([id], [dependencies], factory)
    复制代码
    

    参数:

    id:可选,字符串类型,定义模块标识,如果没有提供参数,默认为文件名
    dependencies:可选,字符串数组,即当前模块所依赖的其他模块,AMD 推崇依赖前置
    factory:必需,工厂方法,初始化模块需要执行的函数或对象。如果为函数,它只被执行一次。如果是对象,此对象会作为模块的输出值
    模块定义和导出

    定义没有依赖的独立模块

    // module1.js
    define({
      increase: function() {},
      getValue: function() {},
    });  
    
    // 或者
    define(function(){
      return {
        increase: function() {},
        getValue: function() {},
      }
    });  
    复制代码
    

    定义有依赖的模块

    // module2.js
    define(['jQuery', 'tool'], function($, tool){
      return {
        clone: $.extend,
        getType: function() {
          return tool.getType();
        }
      }
    });
    复制代码
    

    定义具名模块

    define('module1', ['jQuery', 'tool'], function($, tool){
      return {
        clone: $.extend,
        getType: function() {
          return tool.getType();
        }
      }
    });
    复制代码
    

    引入使用模块

    require(['module1', 'module2'], function(m1, m2){
      m1.getValue();
      m2.getType();
    })
    复制代码
    

    require()函数加载依赖模块是异步加载,这样浏览器就不会失去响应

    AMD规范和CommonJS规范对比
    CommonJS一般用于服务端,AMD一般用于浏览器客户端
    CommonJS和AMD都是运行时加载
    什么是运行时加载?

    CommonJS和AMD模块都只能在运行时确定模块之间的依赖关系
    require一个模块的时候,模块会先被执行,并返回一个对象,并且这个对象是整体加载的
    小结:AMD模块定义的方法能够清晰地显示依赖关系,不会污染全局环境。AMD模式可以用于浏览器环境,允许异步加载模块,也可以根据需要动态加载模块。

    CMD
    CMD(Common Module Definition),通用模块定义,它解决的问题和AMD规范是一样的,只不过在模块定义方式和模块加载时机上不同,CMD也需要额外的引入第三方的库文件,SeaJS CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。

    CMD规范基本语法
    define 是一个全局函数,用来定义模块

    语法:

    define([id], [dependencies], factory)
    复制代码
    

    参数:

    id:可选,字符串类型,定义模块标识,如果没有提供参数,默认为文件名
    dependencies:可选,字符串数组,即当前模块所依赖的其他模块,CMD 推崇依赖就近
    factory:必需,工厂方法,初始化模块需要执行的函数或对象。如果为函数,它只被执行一次。如果是对象,此对象会作为模块的输出值
    模块定义和导出

    除了给 exports 对象增加成员,还可以使用 return 直接向外提供接口

    定义没有依赖的模块

    define(function(require, exports, module) {
      module.exports = {
        count: 1,
        increase: function() {},
        getValue: function() {}
      };
    })
    
    // 或者
    define(function(require, exports, module) {
      return {
        count: 1,
        increase: function() {},
        getValue: function() {}
      };
    })
    复制代码
    

    定义有依赖的模块

    define(function(require, exports, module){
      // 引入依赖模块(同步)
      const module1 = require('./module1');
    
      // 引入依赖模块(异步)
      require.async('./tool', function (tool) {
        tool.getType();
      })
    
      // 暴露模块
      module.exports = {
        value: 1
      };
    })
    复制代码
    

    引入使用模块

    define(function (require) {
      var m1 = require('./module1');
      var m2 = require('./module2');
    
      m1.getValue();
      m2.getType();
    })
    复制代码
    

    CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。

    ES6模块化
    ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及导入和导出的变量。CommonJS 和 AMD 模块,都只能在运行时才能确定。

    ES6模块化语法
    export命令用于暴露模块的对外接口,import命令用于导入其他模块。

    模块定义和导出

    // moduleA.js
    let count = 0;
    
    export const increase = () => {
      count++;
    };
    
    export const getValue = () => {
      return count;
    }
    复制代码
    

    模块引入

    // moduleB.js
    import { getValue, increase } from './moduleA.js';
    
    increase();
    let count = getValue();
    console.log(count);
    
    export function add(val) {
      return val + count;
    }
    复制代码
    

    导入模块时可以给变量或方法指定别名,需要使用as关键字来定义别名

    // moduleB.js
    import { getValue as getCountValue, increase as increaseHandler } from './moduleA.js';
    
    increaseHandler();
    let count = getCountValue();
    console.log(count);
    复制代码
    

    如上例所示,使用import命令的时候,需要知道所要加载的变量名或函数名,否则无法加载。

    为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

    模块默认导出后, 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

    // add.js
    export default function (a, b) {
      return a + b;
    }
    
    // demo.js
    import add from './add';
    console.log(add(1, 2)); // 3
    复制代码
    

    如果想在一条import语句中,同时导入默认方法和其他变量、方法,可以写成下面这样。

    // moduleA.js
    let count = 0;
    
    export const increase = () => {
      count++;
    };
    export const getValue = () => {
      return count;
    }
    export default {
      a: 1
    }
    
    // moduleB.js
    import _, { getValue, increase } from './moduleA.js';
    
    increase();
    let count = getValue();
    console.log(count);
    
    console.log(_);
    复制代码
    

    这种用法在react项目中随处可见

    import React, { useState } from 'react';
    
    function Hello() {
      let [ count, setCount ] = useState(0);
      return (
        <div>
          <p>You click { count } times</p>
          <button onClick={() => setCount(count + 1)}>设置count</button>
        </div>
      )
    }
    复制代码
    

    整体导入

    除了指定加载某些输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

    // moduleB.js
    // import { getValue, increase } from './moduleA.js';
    import * as handler from './moduleA.js';
    
    handler.increase();
    let count = handler.getValue();
    console.log(count);
    复制代码
    

    其他情况

    import命令具有提升效果,会提升到整个模块的头部,首先执行。

    foo();

    import { foo } from ‘./foo.js’;
    复制代码
    上面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。

    由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

    import { 'f' + 'oo' } from './foo'; // 报错
    
    let module = './foo';
    import { foo } from module; // 报错
    
    // 报错
    if (x === 1) {
      import { foo } from './foo1';
    } else {
      import { foo } from './foo2';
    }
    复制代码
    

    上面三种写法都会报错,因为它们用到了表达式、变量和if结构。在静态分析阶段,这些语法都是无法得到值的。

    import语句会执行所加载的模块,因此可以有下面的写法。

    import './initData';
    复制代码
    

    initData.js中会自执行初始化数据的方法,并不需要导出变量和方法。所以只需要import这个模块,执行初始化操作即可。

    ES6 模块与 CommonJS 模块的差异
    它们有两个重大差异:

    1、CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

    2、CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
    CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

    ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

    小结
    由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如类型检验(type system)等只能靠静态分析实现的功能。

    三、总结
    CommonJS规范主要用于服务端编程,加载模块是同步的;在浏览器环境中,同步加载会导致阻塞,所以不适合这个规范,因此有了AMD和CMD规范。

    AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难。

    CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。

    ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

    最后
    如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163相互学习,我们会有专业的技术答疑解惑

    如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star:http://github.crmeb.net/u/defu不胜感激 !

    PHP学习手册:https://doc.crmeb.com
    技术交流论坛:https://q.crmeb.com

    更多相关内容
  • 前端模块化

    千次阅读 2022-02-10 20:24:02
    node.js中的模块化

    模块化

    所有测试代码在gitee上
    地址:https://gitee.com/gaohan888/node-js-learning/tree/master/module

    编程领域中的模块化,就是遵守固定的规则,把一个大文件拆成独立并互相依赖的多个小模块。

    把代码进行模块化拆分的好处:

    ① 提高了代码的复用性

    ② 提高了代码的可维护性

    ③ 可以实现按需加载

    模块化常见的两种

    CommonJS

    • 每个文件都可以当作一个模块
    • 在服务端:模块的加载时运行时同步加载的
    • 在浏览器端:模块需要提前编译打包处理
      在浏览器端编译打包需要用到 browserify。打包命令 browserify app.js -o build.js

    ES6

    依赖模块需要编译打包处理

    语法:
    导入模块:import
    导出模块:export

    分别暴露

    // 分别暴露模块
    export function foo() {
        console.log('foo()');
    }
    
    export function bar() {
        console.log('bar()');
    }
    
    export let arr = [1, 2, 3, 4, 5]
    

    统一暴露

    // 统一暴露
    function fun() {
        console.log('fun()');
    }
    
    function fun2() {
        console.log('fun2()');
    }
    
    export { fun, fun2 }
    

    默认暴露

    // 默认暴露
    export default {
        msg: 'hello......',
        fun: () => {
            console.log('aaaaaaaaaaaaa');
        }
    }
    

    引入模块

    // 引入其他的模块
    // 如果是单个的js文件 引入时要加上后缀
    // 引入的是一个npm下载的包,就不需要加后缀
    import {foo, bar} from './module1.js'
    import {fun, fun2} from './module2.js'
    import module3 from './module3.js'
    import $ from 'jquery'
    import express from 'express'
    
    foo();
    bar();
    fun();
    fun2();
    console.log(module3.msg);;
    console.log(module3.fun());
    

    ES6 中的模块化详细

    模块作用域

    每个模块都有自己的顶级作用域(top-level scope)。换句话说,一个模块中的顶级作用域变量和函数在其他脚本中是不可见的。

    模块代码仅在第一次导入时被解析

    如果同一个模块被导入到多个其他位置,那么它的代码只会执行一次,即在第一次被导入时。

    在一个模块中,“this” 是 undefined

    这是一个小功能,但为了完整性,我们应该提到它。

    在一个模块中,顶级 this 是 undefined。

    将其与非模块脚本进行比较会发现,非模块脚本的顶级 this 是全局对象:

    <script>
      alert(this); // window
    </script>
    
    <script type="module">
      alert(this); // undefined
    </script>
    

    Import *

    通常,我们把要导入的东西列在花括号 import {…} 中,就像这样:

    // 📁 main.js
    import {sayHi, sayBye} from './say.js';
    
    sayHi('John'); // Hello, John!
    sayBye('John'); // Bye, John!
    

    但是如果有很多要导入的内容,我们可以使用 import * as 将所有内容导入为一个对象,例如:

    // 📁 main.js
    import * as say from './say.js';
    
    say.sayHi('John');
    say.sayBye('John');
    

    乍一看,“通通导入”看起来很酷,写起来也很短,但是我们通常为什么要明确列出我们需要导入的内容?

    比如说,我们向我们的项目里添加一个第三方库 say.js,它具有许多函数:
    这里有几个原因。

    • 现代的构建工具(webpack 和其他工具)将模块打包到一起并对其进行优化,以加快加载速度并删除未使用的代码。
      比如说,我们向我们的项目里添加一个第三方库 say.js,它具有许多函数:

      // 📁 say.js
      export function sayHi() { ... }
      export function sayBye() { ... }
      export function becomeSilent() { ... }
      

      现在,如果我们只在我们的项目里使用了 say.js 中的一个函数:

      // 📁 main.js
      import {sayHi} from './say.js';
      

      那么,优化器(optimizer)就会检测到它,并从打包好的代码中删除那些未被使用的函数,从而使构建更小。这就是所谓的“摇树(tree-shaking)”。

    • 明确列出要导入的内容会使得名称较短:sayHi() 而不是 say.sayHi()。

    • 导入的显式列表可以更好地概述代码结构:使用的内容和位置。它使得代码支持重构,并且重构起来更容易。

    不用花括号的导入看起来很酷。刚开始使用模块时,一个常见的错误就是忘记写花括号。所以,请记住,import 命名的导出时需要花括号,而 import 默认的导出时不需要花括号。

    在这里插入图片描述

    node.js中的模块化

    Node.js 中根据模块来源的不同,将模块分为了 3 大类,分别是:

    • 内置模块(内置模块是由 Node.js 官方提供的,例如 fs、path、http 等)
    • 自定义模块(用户创建的每个 .js 文件,都是自定义模块)
    • 第三方模块(由第三方开发出来的模块,并非官方提供的内置模块,也不是用户创建的自定义模块,使用前需要先下载)

    加载模块

    CommonJS的加载方法

    使用强大的 require() 方法,可以加载需要的内置模块、用户自定义模块、第三方模块进行使用。例如:

    请添加图片描述

    注意:使用 require() 方法加载其它模块时,会执行被加载模块中的代码。

    模块作用域

    和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块 作用域。

    请添加图片描述

    好处:

    防止了全局变量污染的问题

    module和module.exports

    module模块存储了和当前模块有关的信息

    在这里插入图片描述

    module.exports

    在自定义模块中,可以使用 module.exports 对象,将模块内的成员共享出去,供外界使用。

    外界用 require() 方法导入自定义模块时,得到的就是 module.exports 所指向的对象。

    使用 require() 方法导入模块时,导入的结果,永远以 module.exports 指向的对象为准。

    在这里插入图片描述

    由于 module.exports 单词写起来比较复杂,为了简化向外共享成员的代码,Node 提供了 exports 对象。默认情况 下,exports 和 module.exports 指向同一个对象。最终共享的结果,还是以 module.exports 指向的对象为准。

    在这里插入图片描述

    exports 和 module.exports 的使用误区

    在这里插入图片描述

    注意:为了防止混乱,建议大家不要在同一个模块中同时使用 exports 和 module.exports

    node.js中的模块化规范

    Node.js 遵循了 CommonJS 模块化规范,CommonJS 规定了模块的特性和各模块之间如何相互依赖。

    CommonJS 规定:

    ① 每个模块内部,module 变量代表当前模块。

    ② module 变量是一个对象,它的 exports 属性(即 module.exports)是对外的接口。

    ③ 加载某个模块,其实是加载该模块的 module.exports 属性。require() 方法用于加载模块。

    npm与包

    Node.js 中的第三方模块又叫做包。

    由于 Node.js 的内置模块仅提供了一些底层的 API,导致在基于内置模块进行项目开发的时,效率很低。

    包是基于内置模块封装出来的,提供了更高级、更方便的 API,极大的提高了开发效率。 包和内置模块之间的关系,类似于 jQuery 和 浏览器内置 API 之间的关系。

    基本示例代码:

    // 导入 moment包
    // 在导包前需要先下载包
    const moment = require('moment')
    
    // 调用moment()方法,得到当前时间
    // 针对当前的时间,调用format() 方法,按照指定的格式进行时间的格式化
    const dt = moment().format('YYYY-MM-DD HH:mm:ss')
    
    console.log(dt);
    

    如果想在项目中安装指定名称的包,需要运行如下的命令:

    npm i 完整的包名称
    

    初次装包完成后,在项目文件夹下多一个叫做 node_modules 的文件夹和 package-lock.json 的配置文件。

    其中:

    node_modules 文件夹用来存放所有已安装到项目中的包。require() 导入第三方包时,就是从这个目录中查找并加载包。

    package-lock.json 配置文件用来记录 node_modules 目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等。

    注意:程序员不要手动修改 node_modules 或 package-lock.json 文件中的任何代码,npm 包管理工具会自动维护它们。

    安装指定版本的包

    默认情况下,使用 npm install 命令安装包的时候,会自动安装最新版本的包。如果需要安装指定版本的包,可以在包 名之后,通过 @ 符号指定具体的版本,例如:

    npm i moment@2.22.2
    

    包的语义化版本规范

    包的版本号是以“点分十进制”形式进行定义的,总共有三位数字,例如 2.24.0 其中每一位数字所代表的的含义如下:

    第1位数字:大版本

    第2位数字:功能版本

    第3位数字:Bug修复版本

    版本号提升的规则:只要前面的版本号增长了,则后面的版本号归零。

    包管理配置文件

    npm 规定,在项目根目录中,必须提供一个叫做 package.json 的包管理配置文件。用来记录与项目有关的一些配置 信息。例如:

    • 项目的名称、版本号、描述等
    • 项目中都用到了哪些包
    • 哪些包只在开发期间会用到
    • 那些包在开发和部署时都需要用到

    在项目根目录中,创建一个叫做 package.json 的配置文件,即可用来记录项目中安装了哪些包。从而方便剔除 node_modules 目录之后,在团队成员之间共享项目的源代码。

    注意:今后在项目开发中,一定要把 node_modules 文件夹,添加到 .gitignore 忽略文件中。

    快速创建 package.json

    npm 包管理工具提供了一个快捷命令,可以在执行命令时所处的目录中,快速创建 package.json 这个包管理 配置文件:

    // 作用:在执行命令所处的目录中,快速新建package.json文件
    npm init -y
    

    注意:

    ① 上述命令只能在英文的目录下成功运行!所以,项目文件夹的名称一定要使用英文命名,不要使用中文,不能出现空格。

    ② 运行 npm install 命令安装包的时候,npm 包管理工具会自动把包的名称和版本号,记录到 package.json 中。

    dependencies 节点

    package.json 文件中,有一个 dependencies 节点,专门用来记录您使用 npm install 命令安装了哪些包。

    在这里插入图片描述

    一次性安装所有的包

    当我们拿到一个剔除了 node_modules 的项目之后,需要先把所有的包下载到项目中,才能将项目运行起来。 否则会报类似于下面的错误:

    // 由于项目运行依赖moment这个包,如果没有提前安装好这个包,就会报如下的错误
    Error: Cannot find module 'moment'
    

    可以运行 npm install 命令(或 npm i)一次性安装所有的依赖包:

    // 执行npm install 命令时, npm 包管理工具会先读取 package.json中 dependencies节点,读取到记录的所有依赖包名称和版本号之后,npm包管理工具会把这些包一次性下载到项目中
    npm install 
    

    卸载包

    可以运行 npm uninstall 命令,来卸载指定的包:

    // 使用npm uninstall 具体的包名 来卸载包
    npm uninstall moment
    

    注意:npm uninstall 命令执行成功后,会把卸载的包,自动从 package.json 的 dependencies 中移除掉

    devDependencies 节点

    如果某些包只在项目开发阶段会用到,在项目上线之后不会用到,则建议把这些包记录到 devDependencies 节点中。

    与之对应的,如果某些包在开发和项目上线之后都需要用到,则建议把这些包记录到 dependencies 节点中。

    您可以使用如下的命令,将包记录到 devDependencies 节点中:

    // 安装指定的包,并记录到devDenoendencies节点中
    npm i 包名 -D
    // 注意:上述命令时简写形式,等价于下面完整的写法
    npm install 包名 --save-dev
    

    切换 npm 的下包镜像源

    nrm

    为了更方便的切换下包的镜像源,我们可以安装 nrm 这个小工具,利用 nrm 提供的终端命令,可以快速查看和切换下 包的镜像源。

    // 通过 npm 包管理器,将nrm安装为全局可用的工具
    npm i nrm -g
    // 查看所有镜像源
    nrm ls
    // 将下包的镜像切换为tapbao镜像
    nrm use taobao
    

    包的分类

    使用 npm 包管理工具下载的包,共分为两大类,分别是:

    • 项目包
    • 全局包

    项目包 那些被安装到项目的 node_modules 目录中的包,都是项目包。

    项目包又分为两类,分别是:

    • 开发依赖包(被记录到 devDependencies 节点中的包,只在开发期间会用到)
    • 核心依赖包(被记录到 dependencies 节点中的包,在开发期间和项目上线之后都会用到)
    npm i 包名 -D # 开发依赖包
    npm i 包名   # 核心依赖包
    

    全局包

    在执行 npm install 命令时,如果提供了 -g 参数,则会把包安装为全局包。 全局包会被安装到

    C:\Users\用户目录\AppData\Roaming\npm\node_modules 目录下。

    // 全局安装指定的包
    npm i 包名 -g
    // 卸载全局安装的包
    npm uninstall 包名 -g
    

    注意:

    ① 只有工具性质的包,才有全局安装的必要性。因为它们提供了好用的终端命令。

    ② 判断某个包是否需要全局安装后才能使用,可以参考官方提供的使用说明即可。

    规范的包结构

    在清楚了包的概念、以及如何下载和使用包之后,接下来,我们深入了解一下包的内部结构。

    一个规范的包,它的组成结构,必须符合以下 3 点要求:

    ① 包必须以单独的目录而存在

    ② 包的顶级目录下要必须包含 package.json 这个包管理配置文件

    ③ package.json 中必须包含 name,version,main 这三个属性,分别代表包的名字、版本号、包的入口。

    开发属于自己的包

    初始化包的基本结构

    ① 新建 gaohan-tools 文件夹,作为包的根目录

    ② 在 gaohan-tools 文件夹中,新建如下三个文件:

    • package.json (包管理配置文件)
    • index.js (包的入口文件)
    • README.md (包的说明文档)

    初始化 package.json

    {
      "name": "gaohan-tools",
      "version": "1.1.0",
      "main": "index.js",
      "description": "提供了格式化时间、HTMLEscape相关的功能",
      "keywords": [
        "itheima",
        "dateFormat",
        "escape"
      ],
      "license": "ISC"
    }
    

    模块化拆分

    ① 将格式化时间的功能,拆分到 src -> dateFormat.js 中

    ② 将处理 HTML 字符串的功能,拆分到 src -> htmlEscape.js 中

    ③ 在 index.js 中,导入两个模块,得到需要向外共享的方法

    ④ 在 index.js 中,使用 module.exports 把对应的方法共享出去

    日期格式化 dateFormat.js

    // 定义格式化时间的函数
    function dateFormat(dateStr) {
      const dt = new Date(dateStr)
    
      const y = dt.getFullYear()
      const m = padZero(dt.getMonth() + 1)
      const d = padZero(dt.getDate())
    
      const hh = padZero(dt.getHours())
      const mm = padZero(dt.getMinutes())
      const ss = padZero(dt.getSeconds())
    
      return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
    }
    
    // 定义一个补零的函数
    function padZero(n) {
      return n > 9 ? n : '0' + n
    }
    
    module.exports = {
      dateFormat
    }
    

    html格式化 htmlEscape.js

    // 定义转义 HTML 字符的函数
    function htmlEscape(htmlstr) {
      return htmlstr.replace(/<|>|"|&/g, match => {
        switch (match) {
          case '<':
            return '&lt;'
          case '>':
            return '&gt;'
          case '"':
            return '&quot;'
          case '&':
            return '&amp;'
        }
      })
    }
    
    // 定义还原 HTML 字符串的函数
    function htmlUnEscape(str) {
      return str.replace(/&lt;|&gt;|&quot;|&amp;/g, match => {
        switch (match) {
          case '&lt;':
            return '<'
          case '&gt;':
            return '>'
          case '&quot;':
            return '"'
          case '&amp;':
            return '&'
        }
      })
    }
    
    module.exports = {
      htmlEscape,
      htmlUnEscape
    }
    
    

    index.js 是包入口文件

    // 这是包的入口文件
    
    const date = require('./src/dateFormat')
    const escape = require('./src/htmlEscape')
    
    // 向外暴露需要的成员
    module.exports = {
      ...date,
      ...escape
    }
    

    编写包的说明文档

    包根目录中的 README.md 文件,是包的使用说明文档。通过它,我们可以事先把包的使用说明,以 markdown 的 格式写出来,方便用户参考。

    README 文件中具体写什么内容,没有强制性的要求;只要能够清晰地把包的作用、用法、注意事项等描述清楚即可。

    我们所创建的这个包的 README.md 文档中,会包含以下 6 项内容:

    安装方式、导入方式、格式化时间、转义 HTML 中的特殊字符、还原 HTML 中的特殊字符、开源协议

    发布包

    注册 npm 账号

    ① 访问 https://www.npmjs.com/ 网站,点击 sign up 按钮,进入注册用户界面

    ② 填写账号相关的信息:Full Name、Public Email、Username、Password

    ③ 点击 Create an Account 按钮,注册账号

    ④ 登录邮箱,点击验证链接,进行账号的验证

    登录 npm 账号

    npm 账号注册完成后,可以在终端中执行 npm login 命令,依次输入用户名、密码、邮箱后,即可登录成功。

    在这里插入图片描述

    注意:在运行 npm login 命令之前,必须 先把下包的服务器地址切换为 npm 的官方 服务器。否则会导致发布包失败!

    把包发布到 npm 上

    将终端切换到包的根目录之后,运行 npm publish 命令,即可将包发布到 npm 上(注意:包名不能雷同)。

    删除已发布的包

    运行 npm unpublish 包名 --force 命令,即可从 npm 删除已发布的包。

    注意:

    ① npm unpublish 命令只能删除 72 小时以内发布的包

    ② npm unpublish 删除的包,在 24 小时内不允许重复发布

    ③ 发布包的时候要慎重,尽量不要往 npm 上发布没有意义的包!

    模块的加载机制

    优先从缓存中加载

    模块在第一次加载后会被缓存。 这也意味着多次调用 require() 不会导致模块的代码被执行多次。

    注意:不论是内置模块、用户自定义模块、还是第三方模块,它们都会优先从缓存中加载,从而提高模块的加载效率。

    内置模块的加载机制

    内置模块是由 Node.js 官方提供的模块,内置模块的加载优先级最高。

    例如,require(‘fs’) 始终返回内置的 fs 模块,即使在 node_modules 目录下有名字相同的包也叫做 fs。

    自定义模块的加载机制

    使用 require() 加载自定义模块时,必须指定以 ./ 或 …/ 开头的路径标识符。在加载自定义模块时,如果没有指定 ./ 或 …/ 这样的路径标识符,则 node 会把它当作内置模块或第三方模块进行加载。

    同时,在使用 require() 导入自定义模块时,如果省略了文件的扩展名,则 Node.js 会按顺序分别尝试加载以下的文件:

    ① 按照确切的文件名进行加载

    ② 补全 .js 扩展名进行加载

    ③ 补全 .json 扩展名进行加载

    ④ 补全 .node 扩展名进行加载

    ⑤ 加载失败,终端报错

    第三方模块的加载机制

    如果传递给 require() 的模块标识符不是一个内置模块,也没有以 ‘./’ 或 ‘…/’ 开头,则 Node.js 会从当前模块的父 目录开始,尝试从 /node_modules 文件夹中加载第三方模块。

    如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。

    例如,假设在 ‘C:\Users\itheima\project\foo.js’ 文件里调用了 require(‘tools’),则 Node.js 会按以下顺序查找:

    ① C:\Users\itheima\project\node_modules\tools

    ② C:\Users\itheima\node_modules\tools

    ③ C:\Users\node_modules\tools

    ④ C:\node_modules\tools

    展开全文
  • Web前端模块化开发教程(ES6+Node.js+Webpack)_源代码.zip
  • 主要介绍了JS前端模块化原理与实现方法,结合实例形式分析了JS前端模块化具体概念、功能、原理、实现方法及相关操作注意事项,需要的朋友可以参考下
  • 前端模块化-总结

    千次阅读 2021-04-25 14:43:02
    因此前端早早就有了模块化技术,可每天醒来前端就多一个名词多一个框架的,发展实在迅猛,就前端模块化这些年的积累就有好几种,我们依次来看看。 commonjs 先看伴随 nodejs 而诞生的 commonjs 规范。commonjs ...

    先说说什么是模块化,就是将独立的功能代码封装成一个独立的文件,其他模块需要使用,在进行引用。

    模块化有利于代码的拆分和架构上的解耦,模块化在服务端领域已经早已成熟,nodejs 也已经支持模块化。

    而在浏览器上,js 脚本是异步载入的,脚本按照编码顺序依次执行,依赖关系只能按照编码顺序来控制。因此前端早早就有了模块化技术,可每天醒来前端就多一个名词多一个框架的,发展实在迅猛,就前端模块化这些年的积累就有好几种,我们依次来看看。

    commonjs

    先看伴随 nodejs 而诞生的 commonjs 规范。commonjs 规范应用于 nodejs 应用中,在 nodejs 应用中每个文件就是一个模块,拥有自己的作用域,文件中的变量、函数都是私有的,与其他文件相隔离。

    CommonJS规范规定,每个模块内部, module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即 module.exports )是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。(引用阮一峰老师的描述)

    举个栗子看看模块化后的文件该怎么写

    // util\index.js
    let name = 'now';
    let age = 18;
    
    let fun = () => {
        console.log('into fun');
        name = 'change'
    }
    
    module.exports = {
        name,
        fun
    }
    console.log(module)
    
    // appJsBridge\index.js
    var { name, fun } = require('./util/index.js')

    上面这个文件有两个变量,一个函数,通过 module.exports 暴露变量 name 和函数 fun ,age 这个变量就是私有的,外部无法直接访问,如果想让 age 变量全局都可以访问,那么可以改成 global.age = 18 ,但这样子会污染全局作用域,会导致意想不到的惊喜(吓)。

    我们看看 util\index.js 打印出来的 module

    图片

    module 中有这些属性

    module.id 模块的识别符,通常是带有绝对路径的模块文件名。module.filename 模块的文件名,带有绝对路径。module.loaded 返回一个布尔值,表示模块是否已经完成加载。module.parent 返回一个module对象,表示调用该模块的模块,如果改该模块没有被引用,那么 parent 就是 null module.children 返回一个module数组,表示该模块要用到的其他模块。module.exports 表示模块对外输出的值。module.paths 这个用于 require 查找该文件的位置。

    在开发中我们常使用的就是 module.exports , 通过 module.exports 输出的对象就是引用方 require 出来的值

    require

    既然有 module.exports 导出,那么就有与之相对应的 require 导入,如下

    var { name, fun, object } = require('./util/index.js') // 不用解构,直接导出对象也可以使用 require 我们最关心的就是文件路径,这里还是引用阮一峰老师的解释

    根据参数的不同格式,require命令去不同路径寻找模块文件。

    1. 如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require('/home/marco/foo.js')将加载/home/marco/foo.js。

    2. 如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require('./circle')将加载当前脚本同一目录的circle.js。

    3. 如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。大家还记得 module.paths 吧,这里就派上用场了。举例来说,脚本/home/user/projects/foo.js执行了require('bar.js')命令,Node会依据 module.paths 路径加上文件名称,依次搜索。这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。

    4. 如果参数字符串不以“./“或”/“开头,而且是一个路径,比如require('example-module/path/to/file'),则将先找到example-module的位置,然后再以它为参数,找到后续路径。

    5. 如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。所以文件名的后缀可以省略。

    6. 如果想得到require命令加载的确切文件名,使用require.resolve()方法。

    module.exports 和 exports

    我们还可以导出 exports 直接使用,但需要注意一点,exports 是已经定义的常量,在导出的时候不能在给它定义,如下

    let exports = module.exports // 错误 #region exports  Identifier 'exports' has already been declaredexports = module.exports; // 正确的

    使用 exports 我们可以这么导出对象,但需要注意一点,在导出对象前不能修改 exports 的指向,若修改 exports 就与 module.exports 不是一个东西了,当然你可以在导出对象后随意修改,这时候就不会影响导出。​​​​​​​

    exports = module.exports// exports = ()=>{} 不能修改exports.fun = () => {    console.log('into fun');    name = 'change'}exports.name = 'now';// exports = ()=>{} 随你改

    单独使用 exports 和 module.exports 其实没啥区别,个人建议还是使用 module.exports ,毕竟这才是常规稳妥的写法。

    隔离性

    commonjs 规范是在运行时加载的,在运行时导出对象,导出的对象与原本模块中的对象是隔离的,简单的说就是克隆了一份。看下面这个栗子​​​​​​​

    // util\index.jslet object = {    age: 10}let fun = function() {    console.log('modules obj', object);    object = { age: 99 }}module.exports = {    fun,    object}
    // index.jsvar { name, fun, object } = require('./util/index.js')console.log('before fun', object)fun()console.log('end fun', object)

    执行 node index.js 看看打印​​​​​​​

    before fun { age: 10 }modules obj { age: 10 }end fun { age: 10 }

    引用方调用了导出的 fun 方法,fun 方法改变了模块中的 object 对象,可是在 index.js 中导出的 object 对象并没有发生改变,所以可见 commonjs 规范下模块的导出是深克隆的。

    在浏览器中使用 commonjs 规范 browserify

    因为浏览器中缺少 module exports require global 这个四个变量,所以在浏览器中没法直接使用 commonjs 规范,非要使用就需要做个转换,使用 browserify ,它是常用的 commonjs 转换工具,可以搭配 gulp webpack 一起使用。看下经过 browserify 处理后的代码,就截取了些关键部分。

    图片

    broswervify

    图片

    我把核心代码复制出来,大致的结构如下,browserify 给每一个模块都设置了一个唯一 id ,通过模块路径来映射模块id,以此来找到各个模块。、

    原本模块中的代码被有 require module exports 这三个参数的函数所包裹,其中 require 用来加载其他模块,exports 用来导出对象。

    !function e(t, n, r) {    function s(o, u) {        if (!n[o]) {            if (!t[o]) {                var a = "function" == typeof require && require;                if (!u && a)                    return a(o, !0);                if (i)                    return i(o, !0);                var f = new Error("Cannot find module '" + o + "'");                throw f.code = "MODULE_NOT_FOUND",                f            }            var l = n[o] = {                exports: {}            };            t[o][0].call(l.exports, function(e) {                var n = t[o][1][e];                return s(n || e)            }, l, l.exports, e, t, n, r)        }        return n[o].exports    }    for (var i = "function" == typeof require && require, o = 0; o < r.length; o++)        s(r[o]);    return s}({    1:[function(require, module, exports) {        "use strict"    },{"babel-runtime/helpers/classCallCheck": 2},[3,4]},    2: [function(require, module, exports) {        "use strict";        exports.__esModule = !0,        exports["default"] = function(instance, Constructor) {            if (!(instance instanceof Constructor))                throw new TypeError("Cannot call a class as a function")        }    }    , {}]},{},[])

    ES6 模块化

    ECMA推出了官方标准的模块化解决方案,使用 export 导出,import 导入,编码简洁,从语义上更加通俗易懂。

    ES6 支持异步加载模块 的模块不是对象,而是在编译的时候就完成模块的引用,所以是编译时才加载的。

    个人认为,ES6模块化是以后的主流。

    还是上面的栗子,用ES6模块化改写,改动上并不大,几个关键字做下修改即可

    // util/index.js
    let name = 'now';
    
    let fun = () => {
        name = 'change'
    }
    
    export {
        name,
        fun
    }
    // app.js
    import { name, fun } from "../util";
    console.log('before fun', object)
    fun()
    console.log('end fun', object)

    浏览器中使用

    但是ES6模块化在浏览器上的支持并不是很好,大部分浏览器还是不支持,所以需要做转换

    1. 不使用 webpack ,使用 gulp 等构建流工具,那么我们需要使用babel将 es6 转成 es5 语法

    使用 babel 转换,在babel 配置文件 .babelrc 写上

    {"presets": ["es2015"]}

    在使用 browserify 对模块规范进行转换。

    1. 若使用 webpack ,webpack 是支持 es6 模块化的,所以就只要引用 babel-loader ,对 es6 的语法做处理即可

    模块的导出是对象的引用

    ES6模块化下的导出是对象的引用,我们看下面这个栗子

    // util/index.js
    let name = 'now';
    
    let fun = () => {
        name = 'change';
    }
    let getName = function() {
        console.log('module:',name)
    }
    
    export {
        name,
        fun,
        getName
    }
    // app.js
    import { name, fun, getName } from "../util";
    console.log("before fun:", name);
    fun();
    console.log("after fun:", name);
    name = "change again";
    getName();

    我们看看输出​​​​​​​

    before fun: nowafter fun: changemodule: change

    可见,模块内部函数改变了模块内的对象,外部导出使用的对象也跟着发生了变化,这一点是和 commonjs 规范区别最大的地方,这个特性可用于状态提升。

    ES6 模块规范和 commonjs 规范 运行机制的区别

    CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

    • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。

    • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

    CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

    AMD-require.js 和 CMD-sea.js

    聊到 AMD 和 CMD 这两个规范都离不开 require.js 和 sea.js,这是早些年,为了解决浏览器异步加载模块而诞生的方案。随着打包工具的发展,commonjs和es6都可以在浏览器上运行了,所以 AMD、CMD 将逐渐被替代。

    AMD规范的模块化:用 require.config()指定引用路径等,用define()定义模块,用require()加载模块。

    CMD规范的模块化:用define()定义模块, seajs.use 引用模块。

    模块兼容处理

    我们开发插件时可能需要对各种模块做支持,我们可以这么处理

    const appJsBridge = function(){};if ("function" === typeof define) {    // AMD CMD    define(function() {        return appJsBridge;    })} else if ("undefined" != typeof exports) {    // commonjs    module.exports = appJsBridge;} else {    // 没有模块化    window.appJsBridge = appJsBridge;}

     

    参考链接

    http://javascript.ruanyifeng.com/nodejs/module.html#toc2 http://www.ruanyifeng.com/blog/2015/05/commonjs-in-browser.html https://juejin.im/post/5aaa37c8f265da23945f365c#heading-5

    展开全文
  • 前端模块化 前端项目构建脚手架 CSS3实战 为什么要前端模块化? 全局变量命名冲突 依赖关系繁琐 业务逻辑模块化,页面内容碎片化 合理抽象模块公共化 === 前端模块化的好处有哪些? 通过异步加载模块,提升页面性能...
  • 前端模块化1

    2022-08-03 22:39:45
    引入模块:import实现区别ES6
模块是动态引用,并且会缓存值,模块面的变绑定其所在的模块。CommonJS
是静态引用CommonJS
模块输出的是一个值
  • 【VUE】前端模块化

    2021-11-18 10:14:17
    为什么要学习前端模块化 因为前端模块化是学习VUE框架的基础,而笔者最近正在攻略VUE这座大山,所以了解前端模块化的知识必不可少。 走进前端模块化 是什么 首先要说明,前端模块化开发并不是一种编程技术,而是一...

    为什么要学习前端模块化

    因为前端模块化是学习VUE框架的基础,而笔者最近正在攻略VUE这座大山,所以了解前端模块化的知识必不可少。
    在这里插入图片描述

    走进前端模块化

    是什么

    首先要说明,前端模块化开发并不是一种编程技术,而是一种编程思想。通过将复杂代码划分为不同功能模块,对不同功能模块进行单独维护,来提升效率,降低成本。

    为什么

    学习前端模块化,我们需要了解

    1. 为什么会出现前端模块化这种思想
    2. 这种编程思想解决了前人编程的哪些痛点

    我们才能更好的理解这种思想的先进和带来的方便。

    1. 为什么会出现前端模块化这种思想
    早期的前端页面都比较简单,就是使用java script进行表单交互,js的组织结构也比较凌乱。而且前后端代码也没有进行分离,前端代码经常和后端的操作搅在一起。因为这样凌乱的,难以维护的方式肯定不利于企业的维护,也不利于代码的复用。所以慢慢的出现了前后端分离和前端的模块化。

    2. 前端模块化的优点

    • 可维护
    • 可复用
    • 方便管理依赖关系(当不同模块/类之间的执行有顺序关系,通俗的说前一个的输出是后一个的输入时,我们需要确保模块/类按顺序执行)
    • 分治思想(将一个N规模的问题分为n个a规模的问题)
    • 减少全局变量污染(当声明了一个全局变量,当在其他文件中出现同名的变量时,会产生冲突)

    怎么做

    为了统一不同开发项目和开发者之间的编程习惯,推出了前端模块化规范。现在有四种模块化规范方案可供同学们选择。

    commonJS模块化规范方案

    在node.js(一个使JavaScript能够脱离浏览器运行的环境,采用了chrome v8 JavaScript引擎,支撑ES6标准)推出后,在最开始的一段时间内只支持commonJS模块化规范。(node.js是commonJS的实现,在commonJS规范的基础上增加了自己的一些新特性实现了自己的模块化系统。先有的commonJS,再有的node,js,node.js将commonJS发扬光大),AMD,CMD等方案都是基于commonJS的发展。commonJS是应用最广泛的模块化规范

    AMD模块化规范方案

    CMD模块化规范方案

    UMD模块化规范方案

    ESM模块化规范方案(重要)

    ECMAScript 6(ES6)推出的模块化方案,是写在ECMA规范里面的,主要就学这个规范就好,是模块化的标准。

    展开全文
  • 前端模块化详解(完整版)

    万次阅读 多人点赞 2019-02-16 19:13:46
    如今CPU、浏览器性能得到了极大的提升,很多页面逻辑迁移到了客户端(表单验证等),随着web2.0时代的到来,Ajax技术得到广泛应用,jQuery等前端库层出不穷,前端代码日益膨胀,此时在JS方面就会考虑使用模块化规范...
  • 前端模块化.xmind

    2020-07-04 12:10:36
    前端模块化.xmind
  • 前端模块化webpack1

    2022-08-03 13:03:18
    前端模块化webpack1
  • 前端模块化方案总结1

    2022-08-08 20:00:34
    另外关于commonjs规范的模块化和ECMAscript模块化的区别es6 { export : '可以输出多个,输出方式为 {}' , e
  • WEB 前端模块化方案完全总结

    千次阅读 2020-09-26 19:51:53
    前端模块化进程中主要经历了下面几种 JavaScript 模块化解决方案: 石器时代 IIFE CJS,CommonJS 模块规范 AMD,CMD,异步模块定义 UMD ESM,ECMAScript 模块系统(ES6) 石器时代 HTML <script>元素用于...
  • 什么是前端模块化

    千次阅读 2019-06-11 18:30:16
    前端模块化 模块化: 是具有特定功能的一个对象( 广义理解 ) 模块定义的流程: 1.定义模块(对象) 2.导出模块 3.引用模块 好处:可以存储多个独立的功能块,复用性高 种类: AMD( require.js) CMD ( sea.js ) ...
  • 今天看了前端模块化的相关概念和演变历史,总结一下。 为什么要模块化? 前端代码复杂度变高,都写在一个文件里不太好,耦合性太高,难维护,全局变量被污染的可能性大。 为了实现模块化,前端开发人员都做了...
  • 前端模块化脚手架,可实现类似java的封装继承多态特性
  • 手机射频前端模块化在产业链上的重要性和一些技术的讲解
  • 模块化主要是指JS模块化,一个JS文件就是一个模块,向外提供特定功能的程序。 模块内部的数据是私有的,向外暴露一些接口与外部其他模块进行通信。 二、模块化 当整个应用JS以模块来编写,这个应用就是一个模块化...
  • 1.前端模块化: 可以理解为一组自定义业务的抽象封装,是根据项目的情况来进行封装组合到一起的,比如我们可以分为登录模块,评论模块。模块可维护性好,组合灵活,方便调用,多人协作互不干扰。 2.前端组件化: ...
  • 前端模块化的作用

    千次阅读 2018-09-17 14:53:17
    相信很多人都用过 seajs、 requirejs ...然而你了解模块化的作用吗?下面主要讲述模块化能解决哪些问题。 命名冲突 做项目时,常常会将一些通用的功能抽象出来,独立成一个个函数,比如 //util.js function form...
  • Web前端模块化开发教程课件(完整版).pptx
  • 在现行的软件架构中,前端和后端是分离的,即前端只专注于页面渲染,而后台专注于业务逻辑,前端和后端是两个不同的工种,而前后端交互最常见的方式就是通过接口。 前后端分离架构 在正式说明前后台架构分离之前,...
  • 05-前端模块化.pdf

    2021-09-18 12:58:09
    05-前端模块化.pdf
  • 前端模块化理解

    千次阅读 2018-08-29 08:45:46
    如今CPU、浏览器性能得到了极大的提升,很多页面逻辑迁移到了客户端(表单验证等),随着web2.0时代的到来,Ajax技术得到广泛应用,jQuery等前端库层出不穷,前端代码日益膨胀,这时候JavaScr...
  • CommonJS 用同步的方式加载模块,在服务端,模块文件都存在本地磁盘,读取速度非常快,所以问题不大。ES6 的模块不是对象,import 命令会被 Java
  • Web前端模块化开发教程(ES6+Node.pptx
  • 前端模块化开发解决方案详解.docx
  • Module-Demo前端模块化demo
  • 前端模块化和组件化理解

    千次阅读 2018-06-30 11:39:40
    模块化主要体现的是一种分而治之的...模块化则是前端最流行的分治手段。 分而治之:将一个大问题分解成多个较为独立的与原问题性质相同的小问题,将所有的小问题的解答组合起来即可得到大问题的答案。 模块化的意义...
  • 专用jq是经过打包的jq,可以用于前端模块化使用,根据实际使用引入,减少资源加载。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 273,768
精华内容 109,507
关键字:

前端模块化

友情链接: heap_allocator.zip