重新看前端模块化

萌芽于面试题之初: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~ 参考链接 前端模块化开发那点历史前端模块化开发的价值前端模块化详解(完整版)require 和 import 区别Dependency inversion principle


JavaScript全屏阅读

下一篇:打酱油

上一篇:钢铁是怎样炼成的

Ctrl + Enter