萌芽于面试题:
import与require的区别?
雏形
入门学习的时候,知道了 function 的语法,声明一块代码片段(code snippet),简单来说就是代码复用。
// index.js
function a() {
// 100 行逻辑
}
function b() {
// 100 行逻辑
}
a、b 可以视为 2 个模块。如果模块越来越多的话,直接约定一些 js 文件即可。目录结构类似这样:
|-- modules
| |-- a.js
| |-- b.js
| |-- ... // 更多的模块
使用时 script 标签引入。模块日益增多,假设有这样的问题:
- 文件依赖。一个模块依赖另一个,使用者并不知道或者忘记引入
- 命名冲突。创建新模块时,使用的变量名覆盖了另一个
一些解决办法
只列举百度到的、稍简易的两个办法:命名空间、IIFE(自执行函数)。
命名空间
参考其它语言的成熟方案是种很好的办法,毕竟 js 就是直接借鉴 JAVA。很多语言都有 namespace 的概念:
// namespace
var org = {},
org.meta = {},
org.meta.com = {},
org.meta.com.utils = {};
org.meta.com.utils.a = function a() {
// 100 行代码
};
根据资料显示,命名空间被当时的前端业界标杆 YUI 采用。讲道理,这框架都没用过。。。
IIFE
IIFE 是 js 中函数立即执行的写法,模块化的关键在于其内部作用域:
(function (global) {
function a() {
// 100 行代码
}
global.a = a;
})(window); // 传入可以是任一对象
写 jQuery 的插件就类似这样,熟悉的代码:
/**
* 这个模块有这样的用法
* ...
* create by xxx
* create at xxxx
*/
(function ($) {
function A() {
// 100 行代码
}
$.fn.A = A;
})(jQuery);
成熟的方案
模块化的规范当属 AMD 与 CMD,对应的实现方式分别是 require.js 和 sea.js。国内用的较多的是 sea.js。
还记得实习的时候,老一辈的前端(10年开始从业)一直在唏嘘
jQuery和sea.js有多火,现在都是三大框架和 es6 了。
AMD - require.js
require.js 核心 API 有两个,定义模块与引入模块:
// module/a.js
// 定义模块
define('module/a', function () {
var name = 'tao';
function say() {
alert(name);
}
// 暴露出去的模块
return {
name,
say,
};
});
还需要一些约定配置来指明模块路径等:
// require.config.js
require.config({
baseUrl: './',
paths: {
'module/a': './module/a.js',
},
});
使用时 require.js 会自动引入需要的模块,通过 script 标签加载,这个过程是异步的。
<script data-main="require.config.js" src="require.js"></script>
<script>
require(['module/a'], function (moduleA) {
// 这里被执行时会看到多了个 script 标签
moduleA.say(); // alert('tao')
});
</script>
CMD - sea.js
万变不离其宗,sea.js 的作者玉伯写的 issue 文章可以说的上是很清楚了,中文对于模块化的诠释基本无出其右。
sea.js 推崇单文件模块:
// module/a.js
// 定义模块
define(function (require, exports, module) {
// 依赖
var moduleDep = require('./dep');
// 100 行代码
// 两种暴露模块的方式
exports.a = a;
module.exports = { a };
});
它同样也有配置可以简化路径、方便记忆:
<script src="sea.js"></script>
<script>
seajs.config({
base: './module/',
alias: {
'module/a': 'a.js',
'main': '../../src/index.js',
},
// debug: true,
});
// 使用时需要借助 `seajs.use`
seajs.use('main');
</script>
官方示例中,将入口文件也定义成了个模块,seajs.use 使用。
其实
require和seajs.use的过程也是异步的,实在不懂为啥两个规范叫法不一样 😄
约定成俗到标准~终焉
当服务端 nodejs 使用的人越来越多,其模块化方案 CommonJS 逐渐约定成俗:
var path = require('path'); // 内部模块
// 引入
var moduleA = require('a');
// 导出
module.exports = {
a: xxx,
};
node 应用因为跑在服务器上,加载模块都是同步的,模块文件都在磁盘上,读取时间忽略不计。但是浏览器里完全不一样,网站应用不可能等模块都下载完后再展示给用户,异步貌似是必然的,AMD/CMD 应运而生。
ECMAScript 6 标准的发布,终于给模块化划上了一个句号:
// 引入
import moduleA from '../module/a.js';
// 导出
export a;
export default b;
真正的浏览器用法,需要使用 module 类型的 script:
<script type="module">
import moduleA from './module/a.js';
if (true) {
// dynamic import
import('./module/b.js').then(({ default: moduleB }) => {
console.log(moduleB);
});
}
// a.js 和 b.js 会被浏览器自动加载,而不是通过 script 标签
</script>
说到最后,import 和 require 的区别,其实就是 ES6 import 和 CommonJS 的区别。
FAQ
import 和 require 的区别
import引入的是值的引用;而require引入的是值的拷贝import是静态的、语言层面上的,在编译时就能确定依赖关系;而require只有在运行时才能
循环引用
某天,你看到了这样的代码:
// getApp.js
import fetch from './utils/request';
const app = {
/** 全局常量配置 */
CONSTATNS: {
method: 'GET',
},
fetch,
};
export default function getApp() {
return app;
}
// utils/request.js
import getApp from '../getApp';
// 因为循环引用,这里拿到的全局对象 app 是个 undefined
const app = getApp();
// 解构赋值报错了
const { method } = app.CONSTANTS; // TypeError undefined
export default function request() {
fetch(url, {
method,
});
};
想要解决上面报错的问题不难,只要延迟调用 getApp 获取配置就行了,比如:
setTimeout(() => {
console.log(getApp()); // app 对象
});
But 循环引用还存在,并木有从根本上解决,以后仍会导致某些隐性问题(假想😂)。遂百度一波流,get 到面向对象设计原则之一 Dependency Inversion Principle:
- High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
大意貌似是指各模块都应该依赖于抽象,这很面向对象。
在 js 的世界中,抽象太抽象了,也不知道怎么说,或者更应该理解为抽象模块,它不会具体做什么,只为降低高低模块耦合性而生。回到上面的问题,尝试写一个抽象模块:
// abstraction.js
/**
* 耦合点就是请求方法,因此,把 `method` 放到
* 抽象模块中。
*
* 实际场景更加复杂。。。
*/
let fetchMethod;
export default function getFetchMethod() {
return fetchMethod;
}
export function setFetchMethod(method) {
fetchMethod = method;
}
接着在两个模块中引入此抽象模块:
// getApp.js
import { setFetchMethod } from './abstraction';
setFetchMethod(app.CONSTATNS.method);
// utils/request.js
import getFetchMethod from './abstraction';
const method = getFetchMethod();
fetch(url, {
method,
});
感觉有点麻烦,干嘛不把 CONSTANTS 抽成个模块呢。。。The end~