前端模块化(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.exports
和exports
指向的不是同一块内存时,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
是什么
这里我们看到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 总结
- Vue的插件是一个对象, 就像
Element
. - 插件对象必须有
install
字段. install
字段是一个函数.- 初始化插件对象需要通过
Vue.use()
.
4.4 扩展学习
Vue.use()
调用必须在new Vue
之前.- 同一个插件多次使用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.$http
为 axios
的别名:(在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组件
*扩展-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选项(对象)