资讯专栏INFORMATION COLUMN

前端模块化杂记

GitCafe / 1733人阅读

摘要:入口模块返回的赋值给总结在剖析了整体的流程之后,可以看到相关的技术细节还是比较清晰的,学无止境引用混合使用详解的语法前端模块化规范

前言

CMDAMD简介

Commonjs简介

Module简介

Common和Module的区别

Module与webpack

Module与Babel

一些问题

总结

引用

前言

前端模块化在近几年层出不穷,有Node的CommonJs,也有属于client端的CMD/AMD模式,而ES6本身也出现了Modules,再加上Webpack以及babel的普及,虽然在代码中经常使用到这些用法,但是如果不去深入研究,总觉得是一个黑魔法,无法探测一些问题的根源。

AMD/CMD简介

事实上,随着打包工具和Babel在前端工程化的世界里大放异彩,AMD/CMD也在逐步退出历史的舞台,这里简单的介绍下其用法及语义。

AMD及其用法

AMD 即Asynchronous Module Definition,中文名是异步模块定义的意思。代表(require.js)

/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //实际路径为js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
  // some code here
});

CMD及其用法

CMD 即Common Module Definition, 中文名是通用模块定义的意思。代表(Sea.js)

/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
    var $ = require("jquery.js");
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});
// 加载模块
seajs.use(["math.js"], function(math){
    var sum = math.add(1+2);
});

两者的区别

1、AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块
2、CMD推崇就近依赖,只有在用到某个模块的时候再去require

Commonjs简介

Commonjs的应用主要是在Node应用中。

通过require引入文件, 文件内部则通过module.export暴露,如下a 就是 module.export

// 引入某个文件
const a = require("some.js")

// some.js

module.export = {
  ...
  // some code
}

除去module.export,Commonjs还有一个exports属性(不推荐使用), 事实上exports就是module.export

// 对外输出接口可以添加变量
var exports = module.exports;
exports.area = function (r) {
  return Math.PI * r * r;
};

exports.circumference = function (r) {
  return 2 * Math.PI * r;
};

// 注意不要直接对exports赋值,这样会切断exports和module的关系
exports = a // 不要这么做
Module简介

ES6的Module是官方正式推出的模块化写法,虽然目前有挺多浏览器还不支持,不过我们可以利用babel将其转换,话不多说,先介绍下Module的基本用法。

ES6的module主要是以import导入想要的对象,export 和 export default导出对象

import x from "some.js"  // 引用some.js中的export default
import {a, b} from "some.js"  // 引用some.js的 export a 和 export b
import x, {a, b} from "some.js"  // 引用 some.js的 export default 和 export a 和 export b

// some.js
const x = () => {}

export const a = () => {}
export const b = () => {}

export default x

因为import是编译时加载,所以import命令具有提升效果,会提升到整个模块的头部,首先执行。

// some code
...
...

import xxx from "xxx" // 提升到最顶部
Common和Module的区别

1. 加载的时机不同

Common是运行时加载的,可以使用变量或者表达式,如:

 const "f" + "oo" =  require("my_modules")

Module是编译时加载的,不可以使用变量或者表达式, 编译时加载效率较高。

2.暴露出的接口不同

Common暴露出来的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
}; 
// main.js
var counter = require("./lib").counter;
var incCounter = require("./lib").incCounter;

console.log(counter);  // 3
incCounter();
console.log(counter); // 3

Module则相反, 输出的是值的引用。

Module与webpack

webpack本身维护了一套模块系统,这套模块系统兼容了所有前端历史进程下的模块规范,包括 amd commonjs es6 等,为了看module在webpack中是怎么运行的,我们可以看一下下面简单的代码:

// webpack

const path = require("path");

module.exports = {
  entry: "./a.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  }
};
// a.js
import a from "./c";

export default "a.js";
console.log(a);
// c.js

export default 333;

打包后的代码如下:

(function(modules) {

  
  function __webpack_require__(moduleId) {
    var module =  {
      i: moduleId,
      l: false,
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    return module.exports;
  }

  return __webpack_require__(0);
})([
  (function (module, __webpack_exports__, __webpack_require__) {

    // 引用 模块 1
    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__c__ = __webpack_require__(1);

/* harmony default export */ __webpack_exports__["default"] = ("a.js");
console.log(__WEBPACK_IMPORTED_MODULE_0__c__["a" /* default */]);

  }),
  (function (module, __webpack_exports__, __webpack_require__) {

    // 输出本模块的数据
    "use strict";
    /* harmony default export */ __webpack_exports__["a"] = (333);
  })
]);

简化一波代码再看,可以看出打包后实际上是一个立即执行函数,并且入参为各个module文件, 最后返回的是__webpack_require__(0)

(function(modules) {

  
  function __webpack_require__(moduleId) {
  }

  return __webpack_require__(0);
})([module1, module2]);

ok, 我们继续看__webpack_require__函数,可以看出它是调用了我们的入口模块,同时传入了module相关的属性,以及函数本身

function __webpack_require__(moduleId) {
    var module =  {
      i: moduleId,
      l: false,
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    return module.exports;
  }

那么继续追溯到入口模块,也就是我们的第一个参数我们可以看到入口模块又调用了 __webpack_require__(1) 去引用入参数组里的第2个函数。
然后会将入参的 webpack_exports 对象添加 default 属性,并赋值。
这里我们就能看到模块化的实现原理,这里的 webpack_exports 就是这个模块的 module.exports 通过对象的引用传参,间接的给 module.exports 添加属性。
最后会将 module.exports return 出来。就完成了 webpack_require 函数的使命。

function (module, __webpack_exports__, __webpack_require__) {

/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__c__ = __webpack_require__(1);

    /* harmony default export */ __webpack_exports__["default"] = ("a.js");
    console.log(__WEBPACK_IMPORTED_MODULE_0__c__["a" /* default */]);

  }

至此,我们可以看出module其实在webpack中,最后的打包结果。

Module与Babel

虽然webpack可以打包转换我们的module,但通常我们都会引入babel来对ES6转成ES5的代码,而Moduel属于ES6,也会被转译。

事实上,babel是将module转换成commonjs,这样 webpack 就无需再做处理,直接使用 webpack 运行时定义的 webpack_require 处理。

不过babel在转换的时候,会有一些特殊的处理, 像下面

首先 export 的时候, 会添加一个__esModule属性到exports,是为了表明这是经过转换的module

export default a

// 转换成  
Object.defineProperty(exports, "__esModule", {
  value: true
});

exports.default = a;

再看 转出的
转出其实会多一个_interopRequireDefault函数,就是为了处理default这个属性

import d from "d"  
// 转化后
var _d = require("d");

var _d2 = _interopRequireDefault(_d);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
一些问题

1.为什么有的地方使用 require 去引用一个模块时需要加上 default?

我们在上文 babel 对导出模块的转换提到,es6 的 export default 都会被转换成 exports.default,即使这个模块只有这一个输出。

2.经常在各大UI组件引用的文档上会看到说明 import { button } from "xx-ui" 这样会引入所有组件内容,需要添加额外的 babel 配置,比如 babel-plugin-component?

import { Button, Select } from "element-ui"
// 转换成
var a = require("element-ui");
var Button = a.Button;
var Select = a.Select;

babel-plugin-component就做了一件事,将 import { Button, Select } from "element-ui" 转换成了

import Button from "element-ui/lib/button"
import Select from "element-ui/lib/select"  

3.我们在浏览一些 npm 下载下来的 UI 组件模块时(比如说 element-ui 的 lib 文件下),看到的都是 webpack 编译好的 js 文件,可以使用 import 或 require 再去引用。但是我们平时编译好的 js 是无法再被其他模块 import 的,这是为什么?

通过 webpack 模块化原理章节给出的 webpack 配置编译后的 js 是无法被其他模块引用的,webpack 提供了 output.libraryTarget 配置指定构建完的 js 的用途。入口模块返回的 module.exports 赋值给 module.exports
总结

在剖析了整体的流程之后,可以看到相关的技术细节还是比较清晰的,学无止境~~~

引用

import、require、export、module.exports 混合使用详解
Module的语法
前端模块化
Commonjs规范

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108907.html

相关文章

  • Webpack系列-第一篇基础杂记

    摘要:系列文章系列第一篇基础杂记系列第二篇插件机制杂记系列第三篇流程杂记前言公司的前端项目基本都是用来做工程化的,而虽然只是一个工具,但内部涉及到非常多的知识,之前一直靠来解决问题,之知其然不知其所以然,希望这次能整理一下相关的知识点。 系列文章 Webpack系列-第一篇基础杂记 Webpack系列-第二篇插件机制杂记 Webpack系列-第三篇流程杂记 前言 公司的前端项目基本都是用...

    Batkid 评论0 收藏0
  • React-flux杂记

    摘要:简介是一种搭建客户端的应用架构,更像是一种模式而不是一个框架。 简介 Flux是一种搭建WEB客户端的应用架构,更像是一种模式而不是一个框架。 特点 单向数据流 showImg(https://segmentfault.com/img/remote/1460000018128072?w=1300&h=708); 与MVC的比较 1.传统的MVC如下所示(是一个双向数...

    王岩威 评论0 收藏0
  • Webpack系列-第三篇流程杂记

    摘要:最后执行了的回调函数,触发了事件点,并回到函数的回调函数触发了事件点执行对于当前模块,或许存在着多个依赖模块。 系列文章 Webpack系列-第一篇基础杂记 Webpack系列-第二篇插件机制杂记 Webpack系列-第三篇流程杂记 前言 本文章个人理解, 只是为了理清webpack流程, 没有关注内部过多细节, 如有错误, 请轻喷~ 调试 1.使用以下命令运行项目,./scrip...

    xorpay 评论0 收藏0
  • webpack系列-插件机制杂记

    摘要:系列文章系列第一篇基础杂记系列第二篇插件机制杂记系列第三篇流程杂记前言本身并不难,他所完成的各种复杂炫酷的功能都依赖于他的插件机制。的插件机制依赖于一个核心的库,。是什么是一个类似于的的库主要是控制钩子函数的发布与订阅。 系列文章 Webpack系列-第一篇基础杂记 Webpack系列-第二篇插件机制杂记 Webpack系列-第三篇流程杂记 前言 webpack本身并不难,他所完成...

    Neilyo 评论0 收藏0
  • React-生命周期杂记

    摘要:前言自从发布之后,更新速度日新月异,而生命周期也随之改变,虽然原有的一些生命周期函数面临废弃,但理解其背后更新的机制也是一种学习在这里根据官方文档以及社区上其他优秀的文章进行一个对于生命周期的总结,大致上分为以下三个模块新老生命周期的区别为 前言 自从React发布Fiber之后,更新速度日新月异,而生命周期也随之改变,虽然原有的一些生命周期函数面临废弃,但理解其背后更新的机制也是一种...

    KoreyLee 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<