JavaScript——模块化

以往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库
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值