模块化
以往JavaScript作为脚本语言给网页添加交互时并没有大规模的代码,所以很多时候这些代码都会写在同一个文件里,这样同时也能减少网络请求次数,加快网页的加载。
在使用其他开发者开发的插件时,则使用多个
在ES6出现之前,有一部分模块化是基于JavaScript语言特性实现的,例如自执行函数、闭包、对象等,也有一部分使用了实现第三方规范(例如AMD、CMD、CommonJS)的工具库,例如require.js、sea.js,不过这些随着ES6规范中的模块化被浏览器、构建工具(Webpack等)和最新的Node.js实现之后,JavaScript的模块化管理现在已经逐渐统一使用ES6规范了。
1、实现模块化的方式
在介绍ES6的模块化语法之前,有几个比较老式、初级的模块化的方式需要介绍一下,因为它们定义起来比较简单,而且仍然会有不少开发者和第三方库在使用,且可以兼容旧版的浏览器,如IE等。
1.1、自执行函数
自执行函数可以形成一个闭包,外部的代码无法访问它里边的变量、函数等,这样可以把自执行函数作为一个模块,在里边定义私有的变量和函数等,把需要暴露给外界的部分通过返回值返回,然后在外部使用变量保存自执行函数的返回值,这样就可以访问它提供的功能了。
假设有一个轮播图插件,它有一个init()方法用于接收要进行轮播的图片数据,有next()和prev()方法用于播放上一张和下一张图片,还有getCurrent()获取当前播放的图片索引,除此之外,它还有内部需要使用的图片数据及改变当前播放索引的方法,这些内部的属性不对外公开,只暴露init()、next()、prev()和getCurrent(),那么使用自执行函数的方式定义的代码如下:
const slider=(function(){
let_data=[];
let current=0;
function getCurrentInRange(current){
return((current%_data.length)+_data.length)%_data.length;
}
return{
init(data){
_data=data;
},
next(){
current=getCurrentInRange(current+1);
},
prev(){
current=getCurrentInRange(current-1);
},
getCurrent(){
return current;
},
};
})();
这个自执行函数在执行后会把结果赋给slider变量,而slider只能访问自执行函数返回的那几种方法,这样就保护了_data和current属性,防止被篡改。
1.2、对象或类
使用对象或者ES6的class也可以创建模块化的代码。JavaScript的Math、Reflect对象就采用这种方式,把一系列的方法放到对象的内部,作为静态成员,在使用的时候,需要加上Math和Reflect这个对象名,相当于定义了命名空间,避免了不同命名空间中的命名冲突。另外通过将对象描述符的writable属性设置为false,或者使用class的私有成员,也可以阻止对属性和方法的访问及修改。
1.3、ES6模块
ES6规范中定义了与模块化相关的语法,一开始浏览器和Node.js并未实现它们,但是在编写本书之时,主流浏览器和Node.js最新版已经支持ES6的模块化语法了。之前在开发前端应用程序时,都使用像Webpack这样的打包工具所提供的ES6模块化实现,来整合分散的JavaScript文件,而Node.js则使用了CommonJS规范,使用不同的语法实现了模块化,由于这种模块化方式仍然广泛地在Node.js应用程序中使用,所以稍后会简单介绍一下它的使用方法。另外需要注意模块化中的代码默认为严格模式。
2、模块化配置
在使用ES6模块化之前,针对浏览器和Node.js需要做一些配置,这样才能将JS文件视为模块并使用ES6模块化语法。
1. 浏览器
在浏览器环境下,需要在
<script src="index.js"type="module"></script>
这样index.js就被视为模块化的JS文件,它导入的其他文件也会自动加载进来,这个示例可以在chapter13/example2中找到。需要注意的是,index.html不能直接双击进行打开,这样它的URL会是file://开头的,而这种形式浏览器是不支持加载模块化的JS文件的,因为有CORS(Cross-Origin Resource Sharing,跨域资源共享)保护,因此只能运行在Web服务器上。VS Code有一个插件叫作LiveServer可以方便地把HTML文件运行在服务器上,安装之后只需要在打开的HTML文件中右击,选择Open with Live Server(用LiveServer打开)选项就可以了。
2. Node.js
在Node.js中使用ES6需要先创建Node.js项目,并将类型设置为module。在安装Node.js后会附带npm命令行工具,在一个空的文件夹下使用命令npm init-y可以生成一个package.json文件,-y会让里边的配置项全部采用默认配置,有了这个文件,Node.js会把这个文件夹当作一个完整的项目。打开生成的package.json文件,里 边是JSON格式的配置项,可以看到项目的名字、版本号、入口文件(main选项)等配置信息,接下来需要添加一个"type"选项,并将它的值设置为"module"。这个项目的示例也在chapter13/example2中,与浏览器配置示例共用一个,如果需要测试Node.js环境,则只需要在项目根目录下运行命令node index.js。
2.1、导出模块
如果.js文件是作为模块使用的,则它里边所定义的变量、函数、对象等都只能在本模块中使用,如果想让其他模块也能够使用,则需要把它们导出。导出变量等只需要在它们定义的前边加上export关键字,代码如下:
export const a=1;
export function add(a,b){return a+b};
export const obj={prop:"value"}
这里直接导出了3项内容,分别是变量a、函数add()和对象obj。更常用的用法是在文件的末尾统一导出,在export后边使用{},并在里边写上要导出的内容,上例也可以使用这种形式,代码如下:
const a=1;
function add(a,b){return a+b};
const obj={prop:"value"}
export{a,add,obj};
这两种导出方式的结果是一样的,它们叫作命名导出,在导入的时候也必须使用相同的名字,例如a、add和obj。另外导出语句只能在顶级代码中,不能在函数或对象里边。
2.2、导入模块
导入由其他模块导出的内容应使用import关键字,后边使用{}在里边写上要导入的内容,最后使用from关键字加上要导入的模块路径。例如,在module2.js文件中,导入之前例子中的a、add和obj的代码如下:
import{a,add,obj}from"./module1.js";
这里需要注意的是,使用ES6语法导入模块需要加上.js扩展名,目录最好使用相对路径,这里的./代表当前目录,…/代表上级目录,如果使用绝对路径的形式,则在更换部署环境后可能会引发问题,例如改为使用子域名访问网站。另外导入的变量、函数、对象等相当于赋值给了常量,所以无法重新给它们赋值,但是可以改变对象中的属性(与常量规则保持一致)。最后,导入语句也必须在顶级代码中。
2.3、默认导出
还有一种导出方式叫作默认导出,它只能导出一项内容,但可以省略名字,之后在导入的时候才需指定一个名字。默认导出使用export关键字加上default,后边跟上要导出的表达式,要注意这里不能导出像const a=1这种定义语句。默认导出用法的代码如下:
function sum(a,b){
return a+b;
}
export default sum;
例子中默认导出了sum()这个函数,也可以直接使用函数表达式作为export default的值,代码如下:
export default function(a,b){
return a+b;
}
这时,在使用import导入其他模块的默认导出时,就不需要再使用{}了,而是可直接指定一个名字,这里的名字可以和默认导出的名字保持一致,也可以使用任何名字,因为默认导出只有一条,所以并不限制名字,代码如下:
import sum from"./sum.js";
//或
import plus from"./sum.js";
有时候,一个模块可能同时有命名导出和默认导出,这是允许的,但是要记住默认导出只能有一个,而命名导出则可以有多个,代码如下:
export function ButtonCircle(){
console.log("圆形按钮");
}
export function ButtonRect(){
console.log("矩形按钮");
}
export default function Button(){
console.log("普通按钮");
}
假设button.js包含了与按钮组件相关的代码,分别使用命名的方式导出了圆形按钮和矩形按钮,使用默认导出的方式导出了普通按钮,其他模块在导入时,可以根据需要导入相关的组件,或者全部导入。全部导入时,可以把默认导入放在前边,然后使用逗号加上{}导入命名导出的部分,代码如下:
import Button,{ButtonCircle,ButtonRect}from"./button.js";
2.4、别名导入
有时候不同模块的命名导出会有相同的名字,这时可以使用as关键字给其中一个或者全部设置别名。或者想给某个模块的导出项另起一个有意义的名字,也可以使用同样的方式。
假设module2.js同样导出了一个add()函数,但是它接收3个参数,用于计算它们的和,如果要导入index.js中,这时就会有两个名为add的导入(与module1.js导出的同名),可以把第2个add另起一个名字,如addForThree,代码如下:
export function add(a,b,c){
return a+b+c;
}
//chapter13/example3/index.js
import{add as addForThree}from"./module2.js";
有一种比较少见的情况,利用别名导入,可以把默认导入也写在{}中,只是由于默认导出没有名字,在{}里边需要使用default关键字代替,这时需要使用as关键字给它起一个别名,例如对于button.js的导入语句也可以写成下方示例的形式,代码如下:
import{default as Button,ButtonCircle,ButtonRect}from"./button.js";
另外,如果想要导入一个模块中的所有导出项目,并放到一个统一的变量中,则可以使用∗并在后边使用as关键字定义存放变量的名字,例如导入button.js中的所有项目,代码如下:
import*as Button from"./button.js";
Button.default; //默认导出
Button.ButtonCircle; //命名导出
Button.ButtonRect; //命名导出
最后关于import,有时可能只想执行某个模块中的一段代码,这个模块可能本身没有导出内容,或者即使有也不想导入它,那么可以直接使用import并在后边加上模块路径,这时导入的模块代码就会并且只会执行一次,也不会导入任何内容。例如假设module3.js中有一行打印日志的代码,在index.js中可以直接把它导入并执行,代码如下:
console.log("hello world");
//chapter13/example3/index.js
import"./module3.js";
这时在运行index.js时,就会打印出"hello world"。
2.5、再导出
有一些模块本身可能会有很多相关的子模块,一般放在单独的文件夹中,为了让其他模块方便导入,通常会在某个模块的根目录下创建一个index.js,然后把其中子模块的导出项目全部导入再导出,这样在其他模块中,只需导入index.js就可以导入其中的所有模块,这样可以方便第三方库的开发者集中导出库中所提供的API。
假设有一个Form表单组件模块,它里边有input、radio和select共3个子组件需要导出,除了可以让需要导入它们的模块分别使用import导入之外,也可以在表单组件中统一对它们进行再导出,然后在导入组件中统一导入,代码如下:
export function Radio(){return"单选按钮"}
//chapter13/example3/form/select.js
export function Select(){return"下拉选项"}
//chapter13/example3/form/input.js
export function InputPwd(){return"密码输入框"}
export function InputCheckbox(){return"复选框"}
export default function InputText(){return"文本输出框"}
//chapter13/example3/form/index.js
export{Select}from"./select.js";
export{Radio}from"./radio.js";
export{default as InputText,InputPwd,InputCheckbox}from"./input.js";
//chapter13/example3/index.js
import{InputText,InputPwd,InputCheckbox,Select,Radio}from"./form/index.js";
示例中,radio.js和select.js分别导出了命名的Radio和Select函数(组件),input.js导出了默认的InputText()和命名的InputPwd()和InputCheckBox()。在form文件夹下有一个index.js文件,里边使用了export…from的语法对3个子模块分别进行了再导出。这里的用法跟import基本一样,只是换成了export关键字。最后在入口的index.js文件中可以直接从form/index.js中导入组件,还可以利用∗导入全部组件:import∗as form from"./form/index.js"。
2.6、动态导入
使用import导入语句会在代码开始执行之前先行加载对应的模块,如果加载了太多模块就会影响代码的执行速度,因为有些导入并不需要立即使用,而是在触发一定的事件或者执行某个函数的时候才去加载,这时可以使用import()函数动态导入相关的模块,它会返回一个Promise,当导入模块代码时,会通过参数传递给then()的回调函数,然后可以使用它的属性访问该模块导出的项目,代码如下:
function handleClickEvent(){
import("./button.js").then((button)=>{
button.default;
button.ButtonCircle;
button.ButtonRect;
});
}
或者也可以使用async/await的形式,代码如下:
async function handleClickEvent(){
let button=await import("./button.js");
}
3、Node.js原生模块管理
Node.js原生的模块管理与ES6的有所不同,它使用了commonJS规范,本节来看一下它的用法。
3.1、导出模块
在Node.js环境中,每个JavaScript文件都内置了module对象,通过给module.exports属性值设置一个对象,并在里边写上要导出的项目就可以进行导出了,代码如下:
const posts=[
{id:1,title:"标题1",content:"内容1"},
{id:2,title:"标题2",content:"内容2"},
{id:3,title:"标题3",content:"内容3"},
];
const getAllTitle=()=>{
return posts.map((post)=>post.title);
};
const getAllContent=()=>{
return posts.map((post)=>post.content);
};
module.exports={getAllTitle,getAllContent};
posts.js模块导出了用于获取全部博客标题和内容的函数,通过给module.exports设置值为{getAllTitle,getAllContent}这样的对象,就可以实现命名导出了,名字就是对象的属性。还有一种简写形式,可以直接使用exports分别导出每个项目,代码如下:
exports.getAllTitle=getAllTitle;
exports.getAllContent=getAllContent;
exports和module.exports指向的是同一个对象,这里可以分别给exports添加属性,这样方便直接导出表达式,例如上边的getAllTitle和getAllContent也可以像下方示例一样进行导出,代码如下:
exports.getAllTitle=()=>{
return posts.map((post)=>post.title);
};
exports.getAllContent=()=>{
return posts.map((post)=>post.content);
};
3.2、导入模块
导入模块使用require()方法,并在里边写上要导入的文件路径,与ES6语法不同的是,路径中的.js后缀名可以省略,在index.js中导入posts.js的代码如下:
const posts=require("./posts");
如果只想导入其中的某个项目,则可以直接在require()后边访问要导入的属性,代码如下:
const getAllContent=require("./posts").getAllContent;
或者使用解构赋值语句,代码如下:
const{getAllTitle}=require("./posts");
通过这种形式也可以在解构赋值时给导入的项目起一个别名。
一般地,在开发Node.js项目时,经常会使用npm安装一些依赖库,这些依赖库会保存到node_modules目录中,如果要导入它们,则可以忽略相对路径部分,直接使用库名进行导入。另外对于Node.js内置的模块,也可以通过这种方式进行导入,代码如下:
const http=require("http"); //导入内置HTTP模块
const express=require("express");//导入express库
1808

被折叠的 条评论
为什么被折叠?



