导图社区 js模块化编程
网站逐渐的发展,嵌入网页的Javascript代码越来越庞大,需要一个团队去分工协作,管理和测试网页的业务逻辑。
编辑于2022-07-11 12:02:00js模块化编程
什么是
模块就是实现特定功能的相互独立的一组方法
为什么
网站逐渐的发展,嵌入网页的Javascript代码越来越庞大
需要一个团队去分工协作,管理和测试网页的业务逻辑
原始的模块定义方法
3种
1. 将相关的多个函数,集中定义在一个js文件中
如何
定义时
//users.js文件中 var 变量=值; ... = ... ; function signin(){ ... ... } functon signup(){ ... ... }
使用时
练习1:
1_users.js
集中存储所有用户相关的函数
1_use_users.html
引入1_users文件,并调用用户相关函数
问题
模块中所有内容都是全局变量和函数。造成全局污染,极易发生冲突
练习2:
1_users.js
中添加getById函数,表示按id查找一个用户
1_products.js
中也添加getById函数,表示按id查找一个商品
1_use_users.html
添加引入1_products.js,并调用getById()
结果:
只有查询商品的输出,没有查询用户的输出,因为被替换了
2. 在一个js文件中,使用对象结构集中存储一组相关的方法
如何
定义时
//users.js文件中 var users={ 属性: 值, ... : ..., signin(){ ... ... }, signup(){ ... ... } }
使用时
进步:
一定程度上减少了全局污染
练习3
1_users.js, 1_products.js
中都将函数,定义在对象中
1_use_user.html
中调用对象的方法,同时访问users和products两个对象的getById()
结果
两个getById()方法没有冲突都调用到了
问题:
将整个对象,暴露在其他程序中,已被篡改
练习4
1_users.js
添加_count:10属性,记录在线人数
添加getCount()方法,获取在线人数
1_use_users.html
先调用getCount()方法,获得在线人数
再直接修改_count属性值,篡改在线人数
结果
对象中的属性,可被随意篡改
3. 使用匿名函数自调,传入依赖的外部对象,返回生成的模块对象
如何
定义时:
var users=(function(){ var 变量=值; ... = ... ; function signin(){ ... ... } function signup(){ ... ... } return { signin, signup }; })()
使用时
进步
外部程序,只能获得return中的部分函数,而未return的,则被隐藏在匿名函数内部,不会被篡改
练习5
1_users.js
用匿名函数自调封装所有内部变量和函数
最后,用return决定哪些内部成员可抛出到外部
1_use_users.html
尝试修改未抛出的_count属性
尝试调用未抛出的getById()函数
结果
只能调用return抛出的,未抛出,就无法访问
问题
对外部全局变量依赖大,比如window和jquery等
解决
要求外部强制传入依赖的全局变量
var users=(function(g , $){ //g就代表全局作用域对象 //$就代表jQuery对象 var 变量=值; ... = ... ; function signin(){ ... ... } function signup(){ ... ... } return { signin, signup }; })(window, jquery)
规范
为什么
让大家互相之间能顺畅的无缝的加载各种模块
包括
CommonJS
什么是
服务器端模块规范
为什么
便于划分nodejs中各种服务器端功能,比如文件读写,网络通信,HTTP支持等
规定
一个单独的js文件就是一个模块
js文件中,module对象,代表当前模块本身
加载模块使用require方法
引入自定义模块,必须加路径前缀,比如./
引入系统内置模块,可直接用模块名
require方法读取一个js文件并执行,最后返回文件内部的module对象的exports对象属性的内容
练习6
2_users.js
定义2_users模块,其中,专门封装用户相关的一组方法
所有方法都封装在一个对象中,最后,将对象整体赋值给module.exports属性
2_use_users.js
引入2_users模块,并解构其中的部分方法,为我所用
强调: 虽然有简写的exports,但只是别名而已,不能完全代替module.exprots
exports=module.exports
如果用.添加成员,则等效于module.exports
如果返回整个替换exports对象,就必须用module.exports属性
练习7
2_users.js
修改module.exports为别名exports
结果:报错
但改为三句exports.signin=function(){... ... };
结果:正确
require请求的模块
使用单例模式创建
首次加载后,缓存起来,再次require时,反复使用同一个模块对象,不再重复创建
练习8
新建2_utils.js
模拟通用工具模块
修改2_users.js和2_use_users.js
都require("2_utils.js")
都在获得的utils对象上判断属性times,并计数
结果
times属性竟然可以累加,说明两次require()只创建了一个utils模块对象,反复使用而已
模块是同步加载
前一个模块加载完,才能加载后续模块,才能执行后续代码
问题:
如果运行在服务端,加载本地js文件模块,则不用担心加载速度问题
如果运行在客户端,要加载远程服务器上的模块,这样就会造成延迟
script默认是同步加载的
前一个script加载不完,后一个script不能开始
解决:
在客户端,采用异步加载模块的方式
AMD
Asynchronous Module Definition
什么是
采用异步方式加载模块
前面模块的加载不影响它后面模块加载或语句的运行。
如果确实有先后的依赖关系:
所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
如何
不是JavaScript原生支持
使用AMD规范进行页面开发需要用到对应的库函数
RequireJS库
为什么
即能异步加载多个js文件
减少网页加载的等待
又能设置某两个js文件前后顺序加载
管理模块间的依赖性
如何
定义子模块
模块必须用特定的define()函数来定义,define()中回调函数的返回值return,决定了模块对外抛出的内容
定义不依赖其它模块的独立子模块
define("模块名",function (){ //成员: 变量/方法 return { 要抛出的成员 } });
定义依赖其它模块的子模块
define("模块名", ['其它模块名',"其它模块名",...], function (参数,参数,...){ //成员: 变量/方法 return { 要抛出的成员 } });
其中,参数指代前面数组中引入的模块
在主js文件中引入子模块:
主js文件依赖于其他子模块,就要使用AMD规范定义的require()函数引入子模块
require(["子模块","子模块",...], function(参数, 参数, ...){ //参数指代子模块对象 //这里可以调用子模块中的成员了 })
HTML中,先引入require.js,并引入主js文件
普通引入
优化require.js本身的异步加载速度
async
表示require.js本身也要异步记载
defer
为了兼容IE,功能同async
强调:
无论是define()还是require()中[]里的模块js文件路径,都是相对于主js文件的
原理:
其实, 无论define()还是require()中写的依赖子模块js,也都是使用普通的<script>加载的。只不过,不是所有script都在开始时创建好的。何时动态创建script,由模块之间的依赖关系决定。
练习9:
3_js/modules/users.js
封装用户相关业务功能的模块
3_js/modules/utils.js
封装通用工具功能的库
3_js/modules/products.js
封装商品相关业务功能的模块
依赖于utils模块
3_js/main.js
主js文件,引入其它子模块
依赖于users.js和products.js
3_amd.html
用script引入require.js文件和主js文件
运行,并观察network中和elements中head元素的变化
发现是按照依赖顺序动态添加的script元素加载的其它模块的js文件
问题1:
引入模块时,都要重复指定路径名
解决:
方法一: 提前配置所有子模块以及更子一级模块的路径,并起别名:
require.config({ paths: { "users":"modules/users", "products":"modules/products", "utils":"modules/utils" //更子一级模块也可定义在paths中 } }); require(["users","products"],function(users, products){ ... })
define([ 'utils' ], function(u) { ... ... });
方法二: 配置基础目录:
require.config({ baseUrl:"3_js/modules/", // paths: { // "users":"users", // "products":"products", // "utils":"utils" // } });
如果确实需要给每个文件其别名,也可以baseUrl和paths同时使用
练习10:
修改main.js, products.js,统一定义路径
问题2:
打开network发现实际上,还是多个script请求,降低了页面加载的效率
解决:
使用requireJS工具,将多个js,按照依赖关系,打包为一个js文件
如何
从requireJS官网,下载r.js工具文件
在r.js所在文件夹运行node命令
node r.js -o baseUrl=./3_js/modules name=../main out=3_js/main-built.js
-o后,跟参数列表
baseUrl=./3_js/modules
定义包含所有子模块文件夹的
name=../main
相对于baseUrl,指定main.js主文件的位置
out=3_js/main-built.js
定义最终生成的一个js文件所在的路径
结果:
在3_js目录下生成了main-built.js
修改html页面中的data-main属性值为3_js/main-built.js
结果:
network中发现除require.js外,只有一个main-built.js请求
练习11:
使用node r.js命令生成main-built.js
修改3_amd.html中data-main属性为main-built.js
运行HTML
观察network和elements中head元素的变化
总结:
主要思想: 提前执行依赖的模块,再执行后续模块
优:
如果前一个依赖模块发生错误,后一个模块不再请求,节约带宽
缺:
无法按需加载模块
比如:
依赖的模块的功能,只有在if判断满足条件时,才被使用。那么,提前执行,会导致即使条件不满足,也必须加载依赖的模块js
练习12
修改3_js/main.js
使用随机数决定调用或不调用products.getById()方法
结果:
即使没用到products模块的功能,也必须提前加载了products模块
解决:
在程序中按需加载js
CMD
Common Module Definition
像CommenJS和AMD的结合
优: 按需加载模块对象
如何:
SeaJS
定义子模块
define(function(require, exports, module) { require()//用法同CommenJS中的require() exports别名//用法同CommenJS中的exports别名用法 module对象//用法同CommenJS中的module对象用法 });
比如:
抛出一个函数
define(function(require, exports, module) { exports.getById=function(){ ... ... } });
抛出一个对象
define(function(require, exports, module) { function signin(){ } function signout(){ } function signup(){ } module.exports={ signin, signout, signup } });
定义主模块,在主模块中,按需加载子模块使用
define(function(require, exports, module){ alert("加载main.js..."); if(Math.random()<0.5){ var {signin,signout,signup}=require("users"); ... ... }else{ alert("没用到users模块..."); } if(Math.random()<0.5){ var {getById}=require("products"); ... ... }else{ alert("没用到products模块..."); } }); seajs.config({ base:"./4_js/modules" }); seajs.use(["./4_js/main"],function(main){})
seajs.use(["主模块名",...],function(main,...){ ... ... })
引入主模块并执行
主模块路径相对于sea.js所在目录
seajs.config({ base: "基础路径", //alias: { // jquery: "jquery.min" //} })
基础路径相对于sea.js所在路径
配置基础路径后,引入子模块时,require()中就都参照基础路径写即可
不定义基础路径,则相对于sea.js所在路径
alias属性作用等于AMD中的paths
require()
用于在需要时引入子模块
如果配置了base,则不必每次都加./
seajs.use() vs require()
seajs.use() 主要用于载入入口js文件
require() 用于模块中引入其它子模块
在自定义脚本中引入主模块
练习13
复制3_js目录,重名名为4_js
修改主模块和每个子模块的定义方式
定义4_cmd.html
引入sea.js和主模块
结果:
按需执行
问题:
只是按需执行,其实,还是在开始时,就加载了全部js文件,没有起到优化的目的
解决:
require.async("子模块",function(模块对象){ ... 后续要执行的代码 ... })
require() vs require.async()
加载方式不同
require() 提前加载完,但暂不执行,等待按需执行
require.async() 异步加载,后续代码放在回调函数中按需执行
加载阶段不同
require() 在代码分析阶段就加载所有模块js,没起到优化带宽的作用
require.async() 在执行阶段,真正按需加载,按需执行
练习14
修改4_js/main.js
用require.async()代替require()
结果
不再一开始加载所有,而是按需加载
合并打包
Grunt
专门压缩,合并,打包的工具
前提:
如果合并,就不能用require.async,动态引入模块,必须用require
因为合并是将所有js文件合并为一个js文件,所以,原来单独的js文件已经找不到了
require中的路径不能偷懒: 同目录必须加./, 下级目录中的js必须: 子目录名/模块名
强调:
main.js中的require路径不能加./
子模块中的require路径前必须加./
如何:
全局安装grunt-cli
npm i -g grunt-cli
安装grunt-cli并不等于安装了 Grunt
Grunt CLI的任务很简单:调用与Gruntfile在同一目录中 Grunt
3部分:
package.json
为项目添加package.json文件
项目根目录: npm init
在package.json中添加配置节
"devDependencies": { "grunt": "^1.0.2", "grunt-cmd-transport": "^0.5.1", "grunt-contrib-clean": "^1.1.0", "grunt-contrib-concat": "^1.0.1", "grunt-contrib-uglify": "^3.3.0" }
devDependencies就是用来描述自身所依赖的模块 其中: grunt模块用来跑Gruntfile.js中定义的任务 grunt-cmd-transport模块用来将seajs中定义的匿名模块变为有名称的模块,并定义依赖关系 grunt-contrib-concat模块用来合并文件 grunt-contrib-uglify模块用来压缩JavaScript grunt-contrib-clean模块用来清除临时目录
安装依赖工具模块
npm install
Gruntfile.js
package.json同目录创建Gruntfile.js
内容:
gruntfile其实就是nodejs一个模块,其中调用grunt.initConfig方法,配置之后每个阶段任务的参数
module.exports = function (grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json'),
使用transport工具,将匿名模块变为具名模块,并识别依赖关系
//找出依赖文件,将依赖的文件由匿名模块变为具名模块(定义moduleID),放在一个临时目录里.build transport:{ options:{ path:['.'] }, modules: { // 对modules文件夹下的所有子js文件,添加模块名并识别依赖关系 options: { idleading: 'modules/' //idleading是定义将来模块名的前缀。将来模块名=idleading+js文件名。目的是为了将来所有js文件合并到一个文件内之后,不同文件夹下的模块间不会重名。 //强调: 要与main中require的路径前缀一致 }, files: [{ expand: true, cwd: '5_js/modules', //目前js文件所在路径 src: '*', //所有js文件,包括子目录下的文件 filter: 'isFile', dest: '.build/5_js/modules' //起名后并添加依赖关系后的新js文件要保存的目标目录 }] }, main: { //对main.js文件,单独改名和识别依赖 files: [{ expand: true, cwd : '5_js', //原始main.js所在文件夹 src : 'main.js', filter : 'isFile', dest : '.build/5_js' //修改后的main.js保存的目标位置 }] } },
用concat工具合并多个js文件为一个文件
concat : { options : { separator: '/*-------每个文件的分割-------*/\n', //定义每个文件内容之间的分隔符 //定义每个文件开头的说明 banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' + '<%= grunt.template.today("yyyy-mm-dd") %> */\n', //定义每个文件的结尾说明 footer: '/*-------合并文件的footer-------*/\n' }, modules: { //先合并transport中生成的.build/modules文件夹下所有js文件为一个modules.js文件 src: ['.build/5_js/modules/*.js','!.build/5_js/modules/*-debug.js'], dest: '.build/5_js/modules/modules.js' }, main : { //再将modules.js和main.js合并为一个main.js文件,放到dist/下 src: ['.build/5_js/main.js','.build/5_js/modules/modules.js'], dest: 'dist/main.js' } },
用uglify工具压缩合并后的main.js文件
uglify : { main : { files : { 'dist/min/main.js' : ['dist/main.js'] //对dist/main.js进行压缩,之后存入dist/min/main.js文件 } } },
强调: 压缩后的文件,无论放在哪里,名字最后保持main.js,和define中使用的主模块名main同名
用clean工具删除合并和压缩过程的中间文件夹内容.build/
clean : { build : ['.build'] //清除.build文件 }
加载并注册每个阶段的工具,并按顺序执行配置好的任务
}); grunt.loadNpmTasks('grunt-cmd-transport'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.registerTask('default',['transport','concat','uglify','clean']); }
执行命令合并,压缩模块代码
打开命令行,在gruntfile.js同目录下,运行grunt命令
结果:
生成合并,压缩后的最终代码
html页面中
强调:
这里["main"]指的是main.js,且必须和main.js中的主模块define("main",...)名称一致
练习15
复制4_js为5_js
修改main
修改require.async为require,取消异步加载,改为全部加载
修改require为: module/users和module/products
强调: 这里要和transport中的idleading一致
修改products中的require为./users
5_cmd.html中
ES6
如何使用ES6中的模块
创建自己的模块
导出模块代码
命名导出
一次导出多个成员
export let 变量名 = 值; export function fun() { ... }
一次性导出一个模块对象
exporte { 成员1,成员2,... };
默认/匿名导出
一次只导出一个变量/一个函数
export default 成员;
成员可能是一个数组或一个函数
导入自己的模块
导入命名导出的自定义模块
直接将模块成员,导入当前模块作用域中
import * from './helpers'; addTax(1000);
将模块的所有成员,导入一个对象中,作为属性和方法使用
import * as h from './helpers'; h.formatPrice(5000);
仅挑选需要的成员导入
import { couponCodes, discountPrice } from './helpers'; discountPrice(500, 0.33);
导入默认导出的自定义模块中唯一一个成员
import 变量名 from './people';
问题: 不支持ES6的模块:
解决: babel-node
安装
全局安装babel-node命令
npm install babel-cli -g
当前项目本地安装语法插件
npm install babel-preset-es2015 -save
定义babel配置文件
项目根目录
.babelrc
{ "presets": [ "es2015" ], "plugins": [] }
运行
babel-node xxx.js
练习16
新建6_js文件夹,其中
utils.js
默认导出一个函数
export default function(){ console.log("执行utils中的fun"); };
products.js
引入utils模块,获得函数u,并定义函数,使用u
import u from "./utils" function getById(){ console.log("按id查询一个商品..."); u(); }
命名导出
export { getById }
users.js
命名导出
function signin(){ console.log("登录..."); } function signout(){ console.log("注销..."); } function signup(){ console.log("注册..."); } export { signin, signout, signup }
main.js
import {signin, signout, signup} from "./users" import * as products from "./products" signin(); signout(); signup(); products.getById();
运行: node 6_js/main.js
出错! 不认识import
解决
安装并配置babel-node
运行:
babel-node 6_js/main.js
成功
es6 转 es5
package.json
"scripts": { ... ... "build": "babel 6_js -d build --out-dir 6_build" },
意为用babel命令,将6_js文件夹下的所有js,转码为es5版本,并保存到6_build目录下
运行
npm run build
练习17
按照以上两步转码
运行: node 6_build/main.js
结果: 正常执行
浏览器支持es6 modules
以下实验必须在服务器环境下htdocs目录下运行
htdocs/下新建modules文件夹
拷贝6_js目录到modules目录下
在modules目录下新建index.html
npm init 新建package.json文件
webpack+babel+es6+浏览器
安装
安装 webpack-cli和babel-cli
npm install -g webpack-cli babel-cli
安装es2015和babel-loader
npm install babel-preset-es2015 babel-loader
安装babel-polyfill
npm install --save-dev babel-polyfill
安装babel-core
npm install -save babel-core
添加两个配置文件
webpack.config.js
module.exports={ entry:['babel-polyfill','./6_js/main.js'], output:{ path:__dirname + "/dist", filename:"main.js" }, module:{ rules:[{ test:/.js$/, loader:"babel-loader" }] } }
.babelrc
{ "presets":["es2015"], "plugins":[] }
练习18:
运行webpack
生成dist目录,其中包含main.js
index.html中
结果: 浏览器正常使用了main.js
启用实验性网络平台功能以支持es6 modules
启用实验性网络平台功能
chrome://flags#enable-experimental-web-platform-features
修改所有import,路径结尾必须都加.js
6_es6.html页面中:
强调: type必须为module
练习19:
按照以上三部设置浏览器,并修改文件
拷贝6_es6.html和6_js文件夹到htdocs目录下
在浏览器中用http://localhost/.../6_es6.hml
vs commonJS
commonJS
对于基本数据类型,属于复制
即会被模块缓存。同时,在另一个模块对该变量赋值,不会影响原模块中的值
练习20
7_cmd_a.js
let count = 1 //基础类型的模块成员 let plusCount = () => { //函数成员 count++ } let getCount=()=>{ //输出原模块内的count console.log("a.js count: ", count); } module.exports = { count, plusCount, getCount }
7_cmd_b.js
let mod = require('./7_cmd_a.js') //复制a中的成员到对象mod中 console.log('b.js count: ', mod.count) mod.getCount() console.log("**********修改a中的count+1*********"); mod.plusCount()//修改a内的count,不影响b中的count console.log('b.js count: ', mod.count) mod.getCount() console.log("**********修改b中的count=3*********"); mod.count=3; console.log('b.js count: ', mod.count) mod.getCount()
结果: 在b中修改mod中的count,原a中的count不变; 修改a中的count,b中mod中的count副本不变
对于复杂数据类型,属于浅拷贝
由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
练习21
修改7_cmd_a.js
第一个行let count=1为一个引用类型对象:
let count={ //引用类型对象 value:1 }
修改plugCount方法:
let plusCount = () => { //函数成员 //count++ count.value++ }
修改7_cmd_b.js
倒数第三行mod.count=3为:
//mod.count = 3 //修改b中的count,不影响a中的count mod.count.value=3;
当使用require命令加载某个模块时,就会运行整个模块的代码。
当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回缓存中第一次创建的模块对象,除非手动清除系统缓存。
练习23
9_cmd_a.js
console.log('a.js ', '开始执行...'); console.log('a.js ', '执行完毕!');
9_cmd_b.js
引入a
console.log('b.js', '开始执行...') console.log("b引入a"); let a = require('./9_cmd_a.js') console.log('b.js', '执行完毕!')
9_cmd_c.js
先后引入a和b
console.log('c.js', '开始执行...') console.log("c引入a..."); let a = require('./9_cmd_a.js') console.log("c引入b..."); let b = require('./9_cmd_b.js') console.log("c.js","执行完毕!");
结果,a只在第一次引用时,执行了一次,之后引入,不重复执行
ES6
ES6模块中的基本/复杂类型都属于【动态只读引用】
不论是基本数据类型还是复杂数据类型, 只要import进来的变量是只读的,。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
对于动态来说,原始值发生变化,import加载的值也会发生变化。不论是基本数据类型还是复杂数据类型
练习22
8_es6_a.js
let count = 1 //基础类型的模块成员 let plusCount = () => { //函数成员 count++ } let getCount=()=>{ console.log("a.js count: ", count); } export { count, plusCount, getCount }
8_es6_b.js
import * as mod from "./8_es6_a.js" //动态只读a中的成员 console.log('b.js count: ', mod.count) mod.getCount() console.log("**********修改a中的count+1*********"); mod.plusCount()//修改a内的count,影响b中的count console.log('b.js count: ', mod.count) mod.getCount() console.log("**********修改b中的count=3*********"); mod.count = 3 //无法在b内修改a中的count,因为只读访问 console.log('b.js count: ', mod.count) mod.getCount()
ES6模块是先分析模块之间的依赖关系,然后再严格按照依赖关系,先后执行。而不是在第一次引入时执行。
练习24:
复制9_cmd_a.js,...b.js, ...c.js
并改名为10_es6_a.js, ...b.js, ...c.js
修改b.js中:
//let a = require('./9_cmd_a.js') import * as a from "./10_es6_a.js";
修改c.js中
//let a = require('./9_cmd_a.js') import * as a from "./10_es6_a.js"; console.log("c引入b..."); //let b = require('./9_cmd_b.js') import * as b from "./10_es6_b.js";
运行,发现a最先被执行一次,然后才执行b,才执行c,这显然是按照依赖顺序执行的