《webpack实战》第二章 模块打包

此文章为阅读《Webpack实战》时结合自己的理解与实践做的笔记,如果错误欢迎指正。

JS中有多种模块标准,首先先以此了解一下它们之间的特性和区别。

2.1 CommonJS

CommonJS为2009年提出的模块标准,Nodejs中使用了CommonJS中的一部分,并在其基础上做了一些调整。

2.1.1 模块

CommonJS中规定每一个文件就是一个模块,与使用script标签将js文件导入的方式不同的是,script标签导入的顶层作用域是全局作用域,而CommonJS模块导入的顶层作用域是模块本身,所有的变量和函数只能自己访问,对外是不可见的。

举个例子:calculator.js

var name = 'calculator.js'

index.js

var name="index.js"
require('./calculator.js')
console.log(name)  //将会打印出index.js  

这说明在calculator.js中声明的变量只会在它的作用域中生效,并不会到index.js的作用域中。

2.1.2 导出

 前面说到CommonJS中的一个模块中的变量只会作用在文件中的作用域,那么想要从模块文件中导出内容就需要使用module.exports。
 对于每个CommonJS文件,都会有一个module对象用于存储该模块的信息,module对象中的exports字段就说明了要对外暴露哪些内容。可以理解成在每个模块前都进行了如下的声明。

var module = { ... }

 通过对module.exports的赋值来暴露变量:

module.exports = {
    id: 'xxxxx',
    add: function (x, y) {
      return x+y
    }
}

 需要注意的是module.exports不代表代码的结束(即不能将其理解成return),其后面的代码还是会继续运行。

2.1.3 导入

 在CommonJS中使用require对模块进行导入。当使用require导入一个模块时会有两种情况:

  • require的模块是第一次被加载,这时会先执行该模块,然后导出内容。
  • require的模块之前被加载过,这时该模块的代码不会再被执行,而是直接导出上次执行的结果。
     也就是说在一个模块中多次导入其他的模块,被导入的模块只会执行一次。
     那假如有c.js,b.js中导入了c.js,a.js中导入了b.js和c.js,那么在执行a.js时c.js会执行多少次呢?
// c.js
module.exports = {
    name: 'c.js'
}
console.log("c.js is running")

// b.js
const cJS = require("./c.js")
console.log("I am b.js, I get " + cJS.name)
module.exports = {
    name: 'b.js'
}

// a.js
const bJS = require("./b.js")
const cJS = require("./c.js")
console.log("I am a.js")

运行后输出将会是什么?答案如下:

c.js is running
I am b.js, I get c.js
I am a.js

原因是require的实现原理使用到了缓存,具体的原理可以看这一篇require的实现。每一个函数都是一个对象,require对象中存着一个名为cache的字段,会将所有导入过的模块以字典的形式记录下来(模块绝对路径->导出的module)。在a.js中:

b和c都没有导入的时候

导入b.js后,在导入c.js前会先查找require.cache[c.js的绝对路径]是否存在,由于c.js已经在cache中存在了,所以会直接使用cache中导出的产物。所以c.js不会再执行一遍。
导入b.js后

2.2 ES6 Module

2.2.1 模块

 ES6 module也是将每个文件作为一个模块,每个模块拥有自身的作用域,和CommonJS不同的是导入导出语句不同,ES6 使用的是import和export。

2.2.2 导出

 在ES6中使用export导出时有两种形式:命名导出和默认导出

  • 命名导出
     一个模块可以有多个命名导出,有两种写法。
export const name = "calculator"
export const add = function(x, y) {return x+y}
const name = "calculator"
const add = function(x, y) {return x+y}
export {name, add as getAdd}

第一种写法是在声明的时候就进行导出,第二种是声明完之后一起导出。在使用命名导时还可以使用as对关键词进行变换。

  • 默认导出
     和命名导出不同的是,默认导出只能有一个,可以看作是导出了一个名为default的变量。注意看default后面直接跟的声明,而不需要变量赋值。
export default function() {}

export default class {...}

2.2.3 导入

  • 对命名导出的导入
// 导出 calculator.js
export const name = "calculator"
export const add = function(x, y) {return x+y}
// 导入
import {name, add as myAdd} from "./calculator.js"

 对于命名导出的导入,import后需要以大括号形式将变量导入进来,并且导入的变量名需要和导出的变量名完全一致,并且这些导入的变量是只读的,不可修改。并且也可以使用as关键字更改导入的变量名。
 在导入多个变量时还可以采取整体导入的方式。

import * as cal from "./calculator.js"
console.log(cal.name)

 这样可以将模块中的所有东西都一次性导入到一个模块变量中(比如例子中的cal),就可以避免污染作用域。

  • 对默认导出的导入
// 导出
export default {
   name: "cal.js"
}
// 导入
import myCal from "./cal.js"
  • 命名导出和默认导出的结合
// a.js
export const add = (x, y) => x+y 
export const name = "a.js"
export default {
    a: 'hi'
}
//b.js 对a的正确导入
import aObj, { add } from "a.js"

//b.js 对a的错误导入
import {add}, obj from "./a"

es6 Module中的导出操作可以使用命名导出和默认导出两种方式,但在导入的时候一定要先导入默认导出的部分。

CommonJS与ES6 Module的区别

动态和静态

CommonJS和ES6 Module最本质的区别在于前者对模块依赖的解决方式是“动态的”,后者是“静态的”。动态的是指模块依赖关系发生在代码运行阶段,静态的则是指模块依赖关系建立在代码编译阶段。这里的理解可以结合webpack的打包结果来看。

// index.js
const cjs = require("./cjs")
import { name } from "./es6"
console.log(cjs.name)
console.log(name)

// es6.js
export const name = "es6"

// cjs.js
module.exports.name = "cjs"

按照生产模式去打包可以得到:

(() => { 
    var o = { 915: o => { o.exports.name = "cjs" } }, // cjs.js
    e = {};
    // require函数
    function r(s) { 
        var t = e[s];
        if (void 0 !== t) return t.exports;
        var n = e[s] = { exports: {} };
        return o[s](n, n.exports, r), n.exports 
    } 
    (() => { 
        "use strict"; 
        const o = r(915); 
        console.log(o.name), 
        // 直接使用es6.js中的name变量
        console.log("es6") })() })();

可以看到CommonJS是在运行时加载的,所以可以使用if语句来判断是否加载某个模块。而在es6 module直接就把模块内容直接写到index.js中了,也就是在代码编译阶段就完成了模块的导入,所以相比CommonJS有以下好处:

  • 死代码的检测和排除,es6在编译阶段就可以知道哪些部分有被使用到,而不会被使用到的代码就是死区,在编译阶段就会去掉。
  • 编译优化,ES6 支持直接导入变量,减少了引用层级。

值拷贝和动态映射

从上面编译结果就可以看出CommonJS的模块导入方式就是类似于工厂模式给传入的对象中的exports属性进行赋值,赋值过程就是浅拷贝。

// cjs.js中的内容
const name = "cjs"
const changeName = () => {
    name += "hi"
}
module.exports.name = name
module.exports.changeName = changeName
// 编译出的结果为
o={915:o=>{
  const e="cjs";
  o.exports.name=e,
  o.exports.changeName=()=>{e+="hi"}}}
// 所以我在外面调用changeName这个函数只会改变这个文件内作用域的变量,导出的name不会随着发生变化
// index.js
import { add, count } from "./es6"
console.log(count)
changeCount()
add()
add()
console.log(count)
// es6.js
export let count = 1
export const add = () => {
    count += 1
}

export const changeCount = () => {
    count = 10
}

编译结果为:

(()=>{"use strict";let o=1;const l=()=>{o+=1};console.log(o),o=10,l(),l(),console.log(o)})();

这里直接把es6中的变量拿到引入模块的作用域中了,所以更改es6.js中的变量就相当于改变当前作用域中变量的效果。【这里不知道是不是因为编译优化导致的打包效果,所以我还不知道es6导入模块是不是通过直接使用当前作用域的方法】

2.3.3 循环依赖

 循环依赖是指A模块和B模块之间互相依赖。首先先看CommonJS中的循环依赖的例子,可以基于前面webpack对CommonJS的编译效果自己想一下输出。

// foo.js
const bar = require('./bar')
console.log(bar)
module.exports = "foo"

// bar.js
const foo = require('./foo')
console.log(foo)
module.exports = "bar"

// index.js
require("./foo")

代码执行的逻辑应该是:

  • 先执行foo.js,执行到require("bar.js"),进入到bar.js中
  • bar.js中执行require('./foo')会从缓存中拿,由于缓存中foo模块的exports初始化成了空对象,所以这里拿到的foo变量为空对象。
    可以通过编译结果看出这个过程
(() => {
  var o = {
      825: (o, r, t) => {
        const e = t(717);
        console.log(e), (o.exports = "bar");
      },
      717: (o, r, t) => {
        const e = t(825);
        console.log(e), (o.exports = "foo");
      },
    },
    r = {};
  !(function t(e) {
    var s = r[e];
    if (void 0 !== s) return s.exports;
    var n = (r[e] = { exports: {} });
    return o[e](n, n.exports, t), n.exports;
  })(717);
})();
// index.js
import foo from "./foo.js"

// foo.js
import bar from "./bar"
console.log(bar)
export default "foo"

// bar.js
import foo from "./foo"
console.log(foo)
export default "bar"
// 编译结果
(()=>{"use strict";console.log(o),console.log("bar");const o="foo"})();

所以打印foo为undefined,打印bar为正常值。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容