本文主要介绍了模块化的概念、由来、优点以及前端开发中常见的模块化规范。
一个模块具有的基本特征:
由于代码量少,所以直接将代码放在标签中即可。
<script> document.getElementById('hello').onClick = function () {
alert('hello'); } document.getElementById('submit-btn').onClick = function () {
var username = document.getElementById('username').value; var password = document.getElementById('password').value; if (!username) {
alert('请输入用户名'); return; } if (!password) {
alert('请输入密码'); return; } // 提交表单 console.log('提交表单'); } </script> 复制代码
随着前端技术的发展,js的广泛应用,代码量日益增加。原始的代码堆砌方式会有很多弊端:
// 张三定义了setValue函数 function setValue(name){
document.getElementById('username').value = name; } // 张三定义了getValue函数 function getValue(){
return document.getElementById('username').value; } // 李四定义了setValue函数 function setValue(name){
document.getElementById('phone').value = name; } // 李四定义了getValue函数 function getValue(){
return document.getElementById('phone').value; } 复制代码
张三定义了setValue和getValue方法,实现了自己的功能,测试了下没有问题
第二天李四增加了功能,也定义了setValue和getValue方法,测试了下自己的功能,也没有问题
第三天,测试给张三提bug了。
所以,这种方式也有弊端:会污染全局命名空间, 容易引起命名冲突,而且模块成员之间看不出依赖
const tool = {
id: 'tool_1', type: 'input', value: '123', getType() {
console.log(`type-${
this.type}`); return this.type; } getValue() {
console.log(`value-${
this.value}`); return this.value; }, } tool.type = 'checkbox' // 直接修改模块内部的数据 tool.getType() // 'checkbox' 复制代码
这样的写法会暴露所有模块成员,内部状态可以被外部改写,导致数据安全问题。
// tool.js文件 (function(window, $) {
let id = '#tool_1'; let type = 'input'; let value = '123'; let count = 0; // 函数 function getType() {
console.log(`type-${
this.type}`); return type; } function getValue() {
console.log(`value-${
$(id).val()}`); return $(id).val(); } function setValue(val) {
value = val; } function increase() {
count++; } // 私有方法 function resetValue() {
value = '123'; } // 私有方法 function resetCount() {
count = 0; } function resetHandler() {
console.log('resetHandler'); resetValue(); resetCount(); } // 暴露方法 window.tool = {
getType, getValue, setValue, increase, resetHandler } })(window, jQuery) 复制代码
引入js时必须保证顺序正确 (index.html文件)
<script type="text/javascript" src="jquery-1.7.2.js"></script> <script type="text/javascript" src="tool.js"></script> <script type="text/javascript"> tool.setValue('567'); </script> 复制代码
上面例子通过jquery方法获取input框的值,所以必须先引入jQuery库,当作参数传入。
Node采用了CommonJS模块规范,但并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性。
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
模块加载的顺序,按照其在代码中出现的顺序。
运行时同步加载
// moduleA.js let count = 0; module.exports.increase = () => {
count++; }; module.exports.getValue = () => {
return count; } 复制代码
模块引入
require命令用于加载模块文件,如果没有发现指定模块,会报错。
可以在一个文件中引入模块并导出另一个模块。
// moduleB.js // 如果参数字符串以“./”或者“../”开头,则表示加载的是一个相对路径的文件 const {
getValue, increase } = require('./moduleA'); increase(); let count = getValue(); console.log(count); module.exports.add = (val) => {
return val + count; } 复制代码
模块标识
模块标识就是require(moduleName)函数的参数moduleName,参数需符合规范:
require也在这个模块的上下文中,用来引入外部模块,其实就是加载其他模块的module.exports属性。
接下来分析下CommonJS模块的大致加载流程
function loadModule(filename, module, require, __filename, __dirname) {
const wrappedSrc = `(function (module, exports, require, __filename, __dirname) {
${
fs.readFileSync(filename, "utf8")} // 使用的是fs.readFileSync,同步读取 })(module, module.exports, require, __filename, __dirname)`; eval(wrappedSrc); } 复制代码
这里只是为了概述加载的流程,很多边界及安全问题都不予考虑,如:
这里我们只是简单的使用 eval来我们的JS代码,实际上这种方式会有很多安全问题,所以真实代码中应该使用 vm来实现。
源代码中还有额外两个参数: __filename和 __dirname,这就是为什么我们在写代码的时候可以直接使用这两个变量的原因。
require实现
function require(moduleName) {
// 通过require.resolve解析补全模块路径,得到一个绝对路径字符串 const id = require.resolve(moduleName); // 先查询下该id路径是否已经缓存到require.cache中,如果已经缓存过了,则直接读缓存 if (require.cache[id]) {
return require.cache[id].exports; } // module 元数据 const module = {
exports: {
}, id, }; // 新加载模块后,将模块路径添加到缓存中,方便后续通过id路径直接读缓存 require.cache[id] = module; // 加载模块 // loadModule(id, module, require); // 直接将上面loadModule方法整合进来 (function (filename, module, require) {
(function (module, exports, require) {
fs.readFileSync(filename, "utf8"); })(module, module.exports, require); })(id, module, require); // 返回 module.exports return module.exports; } require.cache = {
}; require.resolve = (moduleName) => {
/* 解析补全模块路径,得到一个绝对路径字符串 */ return '绝对路径字符串'; }; 复制代码
上面的模块加载时,将module.exports对象传入内部自执行函数中,模块内部将数据或者方法挂载到module.exports对象上,最后返回这个module.exports对象。
以前面的moduleA.js和moduleB.js模块为例:
moduleA 模块中将 increase 和 getValue 方法挂载到 上下文的 module.exports对象上
// moduleA.js let count = 0; module.exports.increase = () => {
count++; }; module.exports.getValue = () => {
return count; } 复制代码
moduleB 模块中 require 了 moduleA,并return 挂载了increase 和 getValue方法的module.exports对象;这个对象经过结构赋值,最终被moduleB中的increase 和 getValue变量接收。
// moduleB.js const {
getValue, increase } = require('./moduleA'); //等价于 // let m = require('./moduleA'); // const getValue = m.getValue; // const increase = m.increase; increase(); let count = getValue(); console.log(count); module.exports.add = (val) => {
return val + count; } 复制代码
function require(moduleName) {
...... // 返回 module.exports return module.exports; } require.resolve = (moduleName) => {
/* 解析补全模块路径,得到一个绝对路径字符串 */ return '绝对路径字符串'; }; 复制代码
在实际项目中,我们经常使用的方式有:
moduleName.js moduleName.json moduleName.node moduleName/index.js moduleName/index.json moduleName/index.node
Nodejs中一个文件是一个模块: module, 一个模块就是一个Module的实例
Nodejs中Module构造函数:
function Module(id, parent){
this.id = id; this.exports = {
}; this.parent = parent; if(parent && parent.children) {
parent.children.push(this); } this.filename = null; this.loaded = false; this.children = []; } //实例化一个模块 var module = new Module(filename, parent); 复制代码
其中id是模块id,exports是这个模块要暴露出来的api接口,parent是父级模块,loaded表示这个模块是否加载完成。
AMD是一个异步模块加载规范,它与CommonJS的主要区别就是异步加载,允许指定回调函数。模块加载过程中即使require的模块还没有获取到,也不会影响后面代码的执行。
由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑异步加载的方式,所以CommonJS规范会比较适用。但是,浏览器环境,要从服务器端下载模块文件,这时就必须采用异步加载,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要早。
语法:
define([id], [dependencies], factory) 复制代码
参数:
定义没有依赖的独立模块
// module1.js define({
increase: function() {
}, getValue: function() {
}, }); // 或者 define(function(){
return {
increase: function() {
}, getValue: function() {
}, } }); 复制代码
定义有依赖的模块
// module2.js define(['jQuery', 'tool'], function($, tool){
return {
clone: $.extend, getType: function() {
return tool.getType(); } } }); 复制代码
定义具名模块
define('module1', ['jQuery', 'tool'], function($, tool){
return {
clone: $.extend, getType: function() {
return tool.getType(); } } }); 复制代码
引入使用模块
require(['module1', 'module2'], function(m1, m2){
m1.getValue(); m2.getType(); }) 复制代码
require()函数加载依赖模块是异步加载,这样浏览器就不会失去响应
语法:
define([id], [dependencies], factory) 复制代码
参数:
除了给 exports 对象增加成员,还可以使用 return 直接向外提供接口
定义没有依赖的模块
define(function(require, exports, module) {
module.exports = {
count: 1, increase: function() {
}, getValue: function() {
} }; }) // 或者 define(function(require, exports, module) {
return {
count: 1, increase: function() {
}, getValue: function() {
} }; }) 复制代码
定义有依赖的模块
define(function(require, exports, module){
// 引入依赖模块(同步) const module1 = require('./module1'); // 引入依赖模块(异步) require.async('./tool', function (tool) {
tool.getType(); }) // 暴露模块 module.exports = {
value: 1 }; }) 复制代码
引入使用模块
define(function (require) {
var m1 = require('./module1'); var m2 = require('./module2'); m1.getValue(); m2.getType(); }) 复制代码
CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。
模块定义和导出
// moduleA.js let count = 0; export const increase = () => {
count++; }; export const getValue = () => {
return count; } 复制代码
模块引入
// moduleB.js import {
getValue, increase } from './moduleA.js'; increase(); let count = getValue(); console.log(count); export function add(val) {
return val + count; } 复制代码
导入模块时可以给变量或方法指定别名,需要使用as关键字来定义别名
// moduleB.js import {
getValue as getCountValue, increase as increaseHandler } from './moduleA.js'; increaseHandler(); let count = getCountValue(); console.log(count); 复制代码
如上例所示,使用import命令的时候,需要知道所要加载的变量名或函数名,否则无法加载。
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
模块默认导出后, 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
// add.js export default function (a, b) {
return a + b; } // demo.js import add from './add'; console.log(add(1, 2)); // 3 复制代码
如果想在一条import语句中,同时导入默认方法和其他变量、方法,可以写成下面这样。
// moduleA.js let count = 0; export const increase = () => {
count++; }; export const getValue = () => {
return count; } export default {
a: 1 } // moduleB.js import _, {
getValue, increase } from './moduleA.js'; increase(); let count = getValue(); console.log(count); console.log(_); 复制代码
这种用法在react项目中随处可见
import React, {
useState } from 'react'; function Hello() {
let [ count, setCount ] = useState(0); return ( <div> <p>You click {
count } times</p> <button onClick={
() => setCount(count + 1)}>设置count</button> </div> ) } 复制代码
整体导入
除了指定加载某些输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
// moduleB.js // import {
getValue, increase } from './moduleA.js'; import * as handler from './moduleA.js'; handler.increase(); let count = handler.getValue(); console.log(count); 复制代码
其他情况
import命令具有提升效果,会提升到整个模块的头部,首先执行。
foo();
由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
import {
'f' + 'oo' } from './foo'; // 报错 let module = './foo'; import {
foo } from module; // 报错 // 报错 if (x === 1) {
import {
foo } from './foo1'; } else {
import {
foo } from './foo2'; } 复制代码
上面三种写法都会报错,因为它们用到了表达式、变量和if结构。在静态分析阶段,这些语法都是无法得到值的。
import语句会执行所加载的模块,因此可以有下面的写法。
import './initData'; 复制代码
initData.js中会自执行初始化数据的方法,并不需要导出变量和方法。所以只需要import这个模块,执行初始化操作即可。
1、CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难。
CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star:http://github.crmeb.net/u/defu不胜感激 !
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/219877.html原文链接:https://javaforall.net
