> 文档中心 > 前端模块化(eport exports module.exports import require)你真的懂了吗?

前端模块化(eport exports module.exports import require)你真的懂了吗?


前端模块化(eport exports module.exports import require)你真的懂了吗?

​ 在前端开发中, 为了提高代码的复用性和可扩展性, 我们可以通过抽取公共组件的方式, 编写公共的模块代码并暴露出去以便在其他组件/页面中进行复用。在编写项目代码的过程中, 我也接触到好几种用于暴露和引用公共代码的方式, 比如: export, export default, module.exports, exports, Vue.prototype, Vue.use, 他们之间有什么区别呢?

1. export & export default & import

1.1 概述- ES6模块规范

export , export default , import 是ES6产物。

历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口(即导出模块),import命令用于输入模块(即导入模块)。

其中使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载,所以ES6为用户提供了更方便的方法export default

1.2 export

用法1

export var firstName = 'Michael';export var lastName = 'Jackson';export var year = 1958;

上面代码ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。

用法2

var firstName = 'Michael';var lastName = 'Jackson';var year = 1958;export { firstName, lastName, year };

上面代码在export命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在var语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。

用法3

export function multiply(x, y) { return x * y;}//export命令除了输出变量,还可以输出函数或类(class)

通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

function v1() { ... }function v2() { ... } export { v1 as streamV1, v2 as streamV2, v2 as streamLatestVersion};//上面代码使用as关键字,重命名了函数v1和v2的对外接口。重命名后,v2可以用不同的名字输出两次。//as的作用 => 将 xxx 重新命名为 xxx

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

// 报错export 1;// 报错var m = 1;export m;//上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量m,还是直接输出 1

也就是说用export 导出模块的时候,如果变量或函数提前定义好了导出是必须加 {}, 如果没有提前定义导出是必须以命名(定义的)的方式

// 报错export 1;// 报错var m = 1;export m;// 正确export var m = 1;// 正确var m = 1;export {m};// 正确var n = 1;export {n as m};// 报错function f() {}export f;// 正确export function f() {};// 正确function f() {}export {f};

1.3 import

注意2点

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

// 报错import{ 'f' + 'oo' } from 'my_module';// 报错let module = 'my_module';import { foo } from module;// 报错if (x === 1) {    import { foo } from 'module1';} else {     import { foo } from 'module2';}

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

export function area(radius) {     return Math.PI * radius * radius;}export function circumference(radius) {    return 2 * Math.PI * radius;}

import 导入上面代码所有值方式

import * as circle from './circle';console.log('圆面积:' + circle.area(4));console.log('圆周长:' + circle.circumference(14));

1.4 export default (语法糖)

export default命令,为模块指定默认输出。export default如果导出变量时必须时匿名,如果导出函数时可以是命名也可以匿名

导出变量

// 正确var a = 1;export default a;//或者export default 1;//这样做没有实际意思// 错误export default var a = 1;

导出函数

export default function () {    console.log('foo');}//或者export default function foo() {    console.log('foo');}// 或者function foo() {    console.log('foo');}export default foo;

export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。

本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。

所以通过以上用法可以总结export default和export区别

1、export default 向外暴露的成员,可以使用任意变量来接收

2、在一个模块中,export default 只允许向外暴露一次

3、在一个模块中,可以同时使用export default 和export 向外暴露成员

4、使用export向外暴露的成员,只能使用{ }的形式来接收,这种形式,叫做【按需导出】

5、export可以向外暴露多个成员,同时,如果某些成员,在import导入时,不需要,可以不在{ }中定义

6、使用export导出的成员,必须严格按照导出时候的名称,来使用{ }按需接收

7、使用export导出的成员,如果想换个变量名称接收,可以使用as来起别名

2. module.exports & exports & require

2.1 概述-node.js产物-CommonJS模块规范

module.exports, exports和require属于node.js产物

Node应用由模块组成,采用CommonJS模块规范。

根据这个规范,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

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

2.2 module.exports

var x = 5;var addX = function (value) {  return value + x;};module.exports.x = x;module.exports.addX = addX;

上面代码通过module.exports输出变量x和函数addX。

2.3 require

require方法用于加载模块。

var example = require('./example.js');console.log(example.x); // 5console.log(example.addX(1)); // 6

2.4 exports 与 module.exports

优先使用 module.exports

为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。

var exports = module.exports;

于是我们可以直接在 exports 对象上添加方法,表示对外输出的接口,如同在module.exports上添加一样。

注意,因为 Node 模块是通过 module.exports 导出的,如果直接将exports变量指向一个值,就切断了exports与module.exports的联系,导致意外发生:

// a.jsexports = function a() {};// b.jsconst a = require('./a.js') // a 是一个空对象

参考the-difference-between-module-exports-and-exports

需要注意的是

require引入的对象本质上是module.exports。这就产生了一个问题,当 module.exportsexports指向的不是同一块内存时,exports的内容就会失效。
例如:

module.exports = {name: '萤火虫叔叔'};exports = {name: '萤火虫老阿姨'}

此时module.exports指向了一块新的内存(该内存的内容为{name: '萤火虫叔叔'}),exports指向了另一块新的内存(该内存的内容为{name: '萤火虫老阿姨'})。require得到的是{name: '萤火虫叔叔'}

附上代码(在main.js中引入people.js):

//people.jsmodule.exports = {name: '萤火虫叔叔'};exports = {name: '萤火虫老阿姨'};
//main.jslet people = require('./people');console.log(people);//输出:{name: '萤火虫叔叔'}

3.两套规范的兼容性

在webpack 2中不允许混用import和module.exports

混合使用的情况

4. Vue.use - Vue插件/全局挂载

Vue.prototype和Vue.use没有关系。

面试的时候总会问如何在Vue的实例上挂载一个方法/属性, 也就是Vue.prototype的小技巧

我想应该是网上有文章写Vue.prototype.$xx是用法的, 但是没有说明Vue.use的用法以及Vue.prototype.$xx为什么就能在组件内this.$xx 这么调用, 所以下面我就细细的说下.

4.1 用饿了么UI举例

下面是饿了么UI的引入代码, 大家对这段应该很熟悉了.

import Vue from 'vue'import Element from 'element-ui'Vue.use(Element)

接下来, 我们在看下这个Element是什么

img

这里我们看到Element是个对象, 上面有version等字段, 其中还有一个install,他是本文的主角, Vue.use就是要运行这个install对应的函数.

4.2 最小结构

写一段最少的代码演示如何用Vue.use初始化插件:

// 插件const plugin = {  install(){    document.write('我是install内的代码')  }}// 初始化插件Vue.use(plugin); // 页面显示"我是install内的代码"

在codepen上看预览点击预览

如果想知道插件的具体实现, 请看 https://cn.vuejs.org/v2/guide…

4.3 总结

  1. Vue的插件是一个对象, 就像Element.
  2. 插件对象必须有install字段.
  3. install字段是一个函数.
  4. 初始化插件对象需要通过Vue.use().

4.4 扩展学习

  1. Vue.use()调用必须在new Vue之前.
  2. 同一个插件多次使用Vue.use()也只会被运行一次.

应用实例: Vue抽取公共组件并全局挂载

5. Vue.prototype.KaTeX parse error: Expected 'EOF', got '&' at position 6: xxx= &̲ this.xxx

5.1 基本的示例

你可能会在很多组件里用到数据/实用工具,但是不想污染全局作用域。这种情况下,你可以通过在原型上定义它们使其在每个 Vue 的实例中可用。

Vue.prototype.$appName = 'My App'

注意这个属性需要在new Vue()之前, 也就是要在main.js文件中进行配置

这样 $appName 就在所有的 Vue 实例中可用了,甚至在实例被创建之前就可以。如果我们运行:

new Vue({  beforeCreate: function () {    console.log(this.$appName)  }})

则控制台会打印出 My App。就这么简单!

5.2 为实例 property 设置作用域的重要性

你可能会好奇:

“为什么 appName 要以 $ 开头?这很重要吗?它会怎样?”

这里没有什么魔法。$ 是在 Vue 所有实例中都可用的 property 的一个简单约定。这样做会避免和已被定义的数据、方法、计算属性产生冲突。

“你指的冲突是什么意思?”

另一个好问题!如果你写成:

Vue.prototype.appName = 'My App'

那么你希望下面的代码输出什么呢?

new Vue({  data: {    // 啊哦,`appName` *也*是一个我们定义的实例 property 名!😯    appName: 'The name of some other app'  },  beforeCreate: function () {    console.log(this.appName)  },  created: function () {    console.log(this.appName)  }})

日志中会先出现 "My App",然后出现 "The name of some other app",因为 this.appName 在实例被创建之后被 data [覆写了](https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/this %26 object prototypes/ch5.md)。我们通过 $ 为实例 property 设置作用域来避免这种事情发生。你还可以根据你的喜好使用自己的约定,诸如 $_appNameΩappName,来避免和插件或未来的插件相冲突。

5.3 真实的示例:通过 axios 替换 Vue Resource

比如你打算替换已经废弃的 Vue Resource 库。你实在是很喜欢通过 this.$http 来访问请求方法,希望换成 axios 以后还能继续这样用。

你需要做的事情是把 axios 引入你的项目:

  • {{ user.name }}

设置 Vue.prototype.$httpaxios 的别名:(在new Vue()之前)

Vue.prototype.$http = axios

然后你就可以在任何 Vue 实例中使用类似 this.$http.get 的方法:

new Vue({  el: '#app',  data: {    users: []  },  created () {    var vm = this    this.$http      .get('https://jsonplaceholder.typicode.com/users')      .then(function (response) { vm.users = response.data      })  }})

5.4 原型方法的上下文

你可能没有意识到,在 JavaScript 中一个原型的方法会获得该实例的上下文。也就是说它们可以使用 this 访问数据、计算属性、方法或其它任何定义在实例上的东西。

让我们将其用在一个名为 $reverseText 的方法上:

Vue.prototype.$reverseText = function (propertyName) {  this[propertyName] = this[propertyName]    .split('')    .reverse()    .join('')}new Vue({  data: {    message: 'Hello'  },  created: function () {    console.log(this.message) // => "Hello"    this.$reverseText('message')    console.log(this.message) // => "olleH"  }})

注意如果你使用了 ES6/2015 的箭头函数,则其绑定的上下文不会正常工作,因为它们会隐式地绑定其父级作用域。也就是说使用箭头函数的版本:

Vue.prototype.$reverseText = propertyName => {  this[propertyName] = this[propertyName]    .split('')    .reverse()    .join('')}

会抛出一个错误:

Uncaught TypeError: Cannot read property 'split' of undefined

5.5 何时避免使用这个模式

只要你对原型 property 的作用域保持警惕,那么使用这个模式就是安全的——保证了这一点,就不太会出 bug。

然而,有的时候它会让其他开发者感到混乱。例如他们可能看到了 this.$http,然后会想“哦,我从来没见过这个 Vue 的功能”,然后他们来到另外一个项目又发现 this.$http 是未被定义的。或者你打算去搜索如何使用它,但是搜不到结果,因为他们并没有发现这是一个 axios 的别名。

这种便利是以显性表达为代价的。当我们查阅一个组件的时候,要注意交代清楚 $http 是从哪来的:Vue 自身、一个插件、还是一个辅助库?

那么有别的替代方案吗?

5.6 替代方案

当没有使用模块系统时

没有模块系统 (比如 webpack 或 Browserify) 的应用中,存在一种任何 重 JS 前端应用都常用的模式:一个全局的 App 对象。

如果你想要添加的东西跟 Vue 本身没有太多关系,那么这是一个不错的替代方案。举个例子:

var App = Object.freeze({  name: 'My App',  version: '2.1.4',  helpers: {    // 这是我们之前见到过的 `$reverseText` 方法    // 的一个纯函数版本    reverseText: function (text) {      return text .split('') .reverse() .join('')    }  }})

如果你在好奇 Object.freeze,它做的事情是阻止这个对象在未来被修改。这实质上是将它的 property 都设为了常量,避免在未来出现状态的 bug。

现在这些被共享的 property 的来源就更加明显了:在应用中的某个地方有一个被定义好的 App 对象。你只需在项目中搜索就可以找到它。

这样做的另一个好处是 App 可以在你代码的任何地方使用,不管它是否是 Vue 相关的。包括向实例选项直接附加一些值而不必进入一个函数去访问 this 上的 property 来得到这些值:

new Vue({  data: {    appVersion: App.version  },  methods: {    reverseText: App.helpers.reverseText  }})

当使用模块系统时

当使用模块系统的时候,你可以轻松地把共享的代码组织成模块,然后把那些模块 require/import 到任何你所需要的地方。这是一个典型的显式做法,因为在每个文件里你都能得到一份依赖清单。你可以准确地知道每个依赖的来历。

虽然毫无疑问它更啰嗦,但是这种方法确实是最可维护的,尤其是当和多人一起协作一个大型应用的时候。

5.7 原理

为什么初始化的时候运行了Vue.prototype.$alert, 然后就可以在任意组件内部运行this.$alert()了呢? 首先要了解构造函数, 实例, 原型(prototype)这3个概念.

构造函数, 实例, 原型(prototype)

这3个概念有点老生常谈了, 百度一搜很多解释, 我先举个例子来和Vue类比你就明白他俩了.

首先我写个假的Vue我们叫他AVue, 恩, 他是个"赝品", “A货”, 接下来跟我一步一步走:

1. AVue是个构造函数

这里我们只模拟下methods功能.

function AVue({methods}){    for(let key in methods){ this[key] = methods[key];    }}

2. 给AVue的原型上放个$alert

AVue.prototype.$alert = ()=>{document.write('我是个赝品!')}

3. 实例化AVue

实例化Vue的时候我们知道, 我们会传入一个对象, 对象里面有data/methods等, 我的AVue一样, 下面我们让AVue也学Vue那样实例化:

// 我只山寨了methods, 所以只能学methods喽const av = new AVue({    methods: { say(){     this.$alert();  }    }});// 调用一下sayav.say(); // 我是个赝品!

在codepen上预览

总结

好了, 运行到这里, 我想你应该看明白了吧, 之前大家写的Vue.prototype.$xx其实只不过是js中函数原型的特性罢了: 函数原型上的属性/方法, 在函数实例化后, 可以在任意实例上读取, 要不你也做个"赝品"试下.

扩展

vue让大家知道了defineProperty, 我们自己也可以用下他, 比如让Vue.prototype变成不可写的, 防止被覆盖.

Object.defineProperty(Vue.prototype, '$alert', {    writable: false,    value(){ console.log('我是行货!')    }});

课后练习

建议大家可以随便写一个vue的插件练手, 比如我的练手项目就是他:
命令式调用vue组件

img2

*扩展-Vue.$xxx

vue中this.$xx的属性详解

this.$el

获取Vue实例关联的DOM元素;vue中也是允许进行dom操作的(但是不建议)

注意this.$el关联的是真实Dom,所以需要在mounted渲染真实Dom之后才可以使用了

this.$refs

获取页面中所有含有ref属性的DOM元素(如vm.$refs.hello,获取页面中含有属性ref = “hello”的DOM元素,如果有多个元素,那么只返回最后一个)

this.$options

获取Vue实例的自定义属性(如this.$options.methods,获取Vue实例的自定义属性methods)

this.$data

获取Vue实例的data选项(对象)

img3