Artikel ini membawakan anda pengetahuan yang berkaitan tentang modularisasi dalam JavaScript saya harap ia akan membantu anda.
Seperti yang kita sedia maklum, status js dalam pembangunan front-end. Sangat penting untuk mempelajarinya dengan baik.
Artikel berikut memperkenalkan modulariti.
Apakah sebenarnya modularisasi dan pembangunan modular?
Malah, matlamat utama pembangunan modular adalah untuk membahagikan program kepada struktur kecil.
Tulis kod logik anda sendiri dalam struktur ini, yang mempunyai skopnya sendiri dan tidak akan menjejaskan struktur lain.
Struktur ini boleh mengeksport pembolehubah, fungsi, objek, dsb. yang anda ingin dedahkan kepada strukturnya untuk digunakan.
Anda juga boleh mengimport pembolehubah, fungsi, objek, dsb. dalam struktur lain dalam beberapa cara.
Struktur yang dinyatakan di atas ialah modul;
Pada masa awal pembangunan web, Brendan Eich membangunkan JavaScript hanya sebagai bahasa skrip untuk melakukan beberapa pengesahan bentuk mudah atau pelaksanaan animasi Pada masa itu, masih ada kod yang sangat sedikit.
Pada masa ini kita hanya perlu menulis kod JavaScript ke
dan tidak perlu menulisnya dalam berbilang fail.
Tetapi dengan perkembangan pesat bahagian hadapan dan JavaScript, kod JavaScript telah menjadi lebih dan lebih kompleks:
Kemunculan ajax, bahagian hadapan dan bahagian belakang Pemisahan pembangunan bermakna selepas bahagian belakang mengembalikan data, kami perlu memaparkan halaman bahagian hadapan melalui JavaScript.
Dengan kemunculan SPA, halaman hadapan menjadi lebih kompleks: satu siri keperluan kompleks termasuk penghalaan bahagian hadapan, pengurusan negeri, dsb. perlu dilaksanakan melalui JavaScript.
Termasuk pelaksanaan Node, program back-end kompleks yang ditulis dalam JavaScript, kekurangan modulariti adalah kecacatan yang membawa maut.
Jadi, modularisasi sudah menjadi keperluan yang sangat mendesak untuk JavaScript. Itulah sebabnya ES6 (2015) melancarkan penyelesaian modularnya sendiri.
Sebelum ini, untuk membolehkan JavaScript menyokong pemodulatan, banyak spesifikasi pemodulatan yang berbeza muncul: AMD, CMD, CommonJS, dsb.
Seperti konflik penamaan.
Selesaikan masalah di atas melalui ungkapan invokasi fungsi segera (IIFE). Kerana fungsi mempunyai skopnya sendiri, ia tidak akan menyebabkan konflik penamaan antara fail yang berbeza.
// a.js var moduleA = (function() { var name = "llm" var age = 22 var isFlag = true return { name: name, isFlag: isFlag } })()
// b.js var moduleB = (function() { var name = "zh" var isFlag = false return { name: name, isFlag: isFlag } })()
// 使用 moduleA.name moduleB.name
Walau bagaimanapun, kami sebenarnya membawa masalah baharu:
Saya perlu mengingati penamaan objek yang dikembalikan dalam setiap modul, Sahaja dengan cara ini ia boleh digunakan dengan betul apabila menggunakan modul lain.
Kod ini mengelirukan untuk ditulis dan kod dalam setiap fail perlu dibalut dengan fungsi tanpa nama untuk menulis.
Jika tiada spesifikasi yang sesuai, semua orang dan setiap syarikat boleh menamakan modul dengan sewenang-wenangnya, atau mempunyai nama modul yang sama.
Jadi, kami akan mendapati bahawa walaupun modularisasi dicapai, pelaksanaan kami terlalu mudah dan tidak standard.
Kita perlu merumuskan spesifikasi tertentu untuk mengekang semua orang menulis kod modular mengikut spesifikasi ini. Spesifikasi ini harus termasuk fungsi teras: modul itu sendiri boleh mengeksport sifat terdedah dan modul boleh mengimport sifat yang diperlukannya. Untuk menyelesaikan masalah di atas, komuniti JavaScript telah muncul dengan satu siri spesifikasi yang berguna Seterusnya, kita akan mempelajari beberapa spesifikasi yang mewakili.
Kita perlu tahu bahawa CommonJS pada asalnya dicadangkan untuk digunakan di luar penyemak imbas dan dinamakan ServerJS pada masa itu, untuk mencerminkannya breadth , diubah suai kepada CommonJS, kami biasanya memanggilnya sebagai singkatan CJS.
Node ialah pelaksanaan perwakilan CommonJS di bahagian pelayan.
Browserify ialah pelaksanaan CommonJS dalam penyemak imbas.
Alat pembungkusan webpack mempunyai sokongan dan penukaran untuk CommonJS.
Oleh itu, CommonJS disokong dan dilaksanakan dalam Node, membolehkan kami menjalankan pembangunan modular dengan mudah semasa pembangunan nod.
Setiap fail js dalam Node ialah modul yang berasingan.
Modul ini termasuk pembolehubah teras spesifikasi CommonJS: eksport, module.exports, memerlukan.
Kita boleh menggunakan pembolehubah ini untuk memudahkan pembangunan modular.
Kami telah menyebut sebelum ini bahawa teras modularisasi ialah eksport dan import, yang dilaksanakan dalam Node:
eksport dan modul boleh bertanggungjawab untuk mengeksport kandungan dalam modul.
Fungsi memerlukan boleh membantu kami mengimport kandungan daripada modul lain (modul tersuai, modul sistem, modul perpustakaan pihak ketiga).
CommonJS disokong dan dilaksanakan dalam Node, membolehkan kami menjalankan pembangunan modular dengan mudah semasa pembangunan nod:
在Node中每一个js文件都是一个单独的模块。
这个模块中包括CommonJS规范的核心变量:exports、module.exports、require。
exports和module.exports可以负责对模块中的内容进行导出。
require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容。
下面我们将来介绍exports、module.exports、require的使用。
exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出。
我们也可以通过module.exports直接导出一个对象。
我们通过require()函数导入一个文件。并且该文件导出的变量。
下面来详细介绍一个module.exports。
CommonJS中是没有module.exports的概念的。
但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是module。
所以在Node中真正用于导出的其实根本不是exports,而是module.exports。
因为module才是导出的真正实现者。
并且内部将exports赋值给module.exports。
该方式的导入导出有以下特点:
Node中的文件都运行在一个函数中。可以通过打印console.log(arguments.callee + "")来验证。
导入导出是值的引用,如果导出的是一个基本数据类型值,那么导出文件改变该值,然后导入文件该变量的值也不会变。
// a.js const obj = require("./b.js") console.log(obj) setTimeout(() => { obj.name = "llm" }, 1000)
// b.js const info = { name: "zh", age: 22, foo: function() { console.log("foo函数~") } } setTimeout(() => { console.log(info.name) // llm }, 2000) module.exports = info
他是通过require 函数来导入的,只有在执行js代码才会知道模块的依赖关系。
代码是同步执行的。
模块多次引入,只会加载一次。每个module内部会存在一个loaded来确定是否被加载过。
代码循环引入的时候,深度优先来加载模块。然后再广度优先。
我们现在已经知道,require是一个函数,可以帮助我们引入一个文件(模块)中导出的对象。
那么,require的查找规则是怎么样的呢?
详细查找规则,请访问这里
这里我总结比较常见的查找规则:导入格式如下:require(X)
模块在被第一次引入时,模块中的js代码会被运行一次
模块被多次引入时,会缓存,最终只加载(运行)一次
为什么只会加载运行一次呢?
这是因为每个模块对象module都有一个属性:loaded。为false表示还没有加载,为true表示已经加载。
如果有循环引入,那么加载顺序是什么?
如上图,Node采用的是深度优先算法:main -> aaa -> ccc -> ddd -> eee ->bbb
CommonJS加载模块是同步的:
同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行。
这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快。
如果将它应用于浏览器呢?
浏览器加载js文件需要先从服务器将文件下载下来,之后再加载运行。
那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作。所以在浏览器中,我们通常不使用CommonJS规范。当然在webpack中使用CommonJS是另外一回事。因为它会将我们的代码转成浏览器可以直接执行的代码。
在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD。但是目前一方面现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者ES Module代码的转换。AMD和CMD已经使用非常少了,所以这里我们进行简单的演练。
AMD主要是应用于浏览器的一种模块化规范:
AMD是Asynchronous Module Definition(异步模块定义)的缩写。它采用的是异步加载模块。
我们提到过,规范只是定义代码的应该如何去编写,只有有了具体的实现才能被应用。
AMD实现的比较常用的库是require.js和curl.js。
require.js的使用
定义HTML的script标签引入require.js和定义入口文件。data-main属性的作用是在加载完src的文件后会加载执行该文件
// index.html <script src="./require.js" data-main="./index.js"></script>
//main.js require.config({ baseUrl: '', // 默认是main.js的文件夹路径 paths: { foo: "./foo" } }) require(["foo"], function(foo) { console.log("main:", foo) })
// foo.js define(function() { const name = "zh" const age = 22 function sum(num1, num2) { return num1 + num2 } return { name, age, sum } })
CMD规范也是应用于浏览器的一种模块化规范:
CMD 是Common Module Definition(通用模块定义)的缩写。它也采用了异步加载模块,但是它将CommonJS的优点吸收了过来。
AMD实现的比较常用的库是SeaJS。
SeaJS的使用
引入sea.js和使用主入口文件。
// index.html <script src="./sea.js"></script> <script> seajs.use("./main.js") </script>
//main.js define(function(require, exports, module) { const foo = require("./foo") console.log("main:", foo) })
// foo.js define(function(require, exports, module) { const name = "zh" const age = 22 function sum(num1, num2) { return num1 + num2 } // exports.name = name // exports.age = age module.exports = { name, age, sum } });
ES Module和CommonJS的模块化有一些不同之处:
一方面它使用了import和export关键字来实现模块化。
另一方面它采用编译期的静态分析,并且也加入了动态引用的方式。
export负责将模块内的内容导出。
import负责从其他模块导入内容。
采用ES Module将自动采用严格模式:use strict。
基本使用
// index.html <script src="./main.js" type="module"></script>
// foo.js let obj = { name: "zh", age: 22 } export default sum
// main.js import foo from './foo.js' console.log(foo)
在html文件加载入口文件的时候,需要指定type为module。
在打开html文件时,需要开启本地服务,而不能直接打开运行在浏览器上。
这个在MDN上面有给出解释:
你需要注意本地测试 — 如果你通过本地加载Html 文件 (比如一个 file:// 路径的文件), 你将会遇到 CORS 错误,因为Javascript 模块安全性需要。
你需要通过一个服务器来测试。
export关键字将一个模块中的变量、函数、类等导出。
我们希望将其他中内容全部导出,它可以有如下的方式:
方式一:在语句声明的前面直接加上export关键字。
export const name = "zh" export const age = 22
方式二:将所有需要导出的标识符,放到export后面的 {} 中。注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的。所以: export {name: name},是错误的写法。
const name = "zh" const age = 22 function foo() { console.log("foo function") } export { name, age, foo }
方式三:导出时给标识符起一个别名。(基本没用,一般在导入文件中起别名)。然后在导入文件中就只能使用别名来获取。
export { name as fName, age as fAge, foo as fFoo }
import关键字负责从另外一个模块中导入内容。
导入内容的方式也有多种:
方式一:import {标识符列表} from '模块'。注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容。
import { name, age } from "./foo.js"
方式二:导入时给标识符起别名。
import { name as fName, age as fAge } from './foo.js'
方式三:通过 * 将模块功能放到一个模块功能对象(a module object)上。然后通过起别名来使用。
import * as foo from './foo.js'
表示导入导出。
import { add, sub } from './math.js' import {otherProperty} from './other.js' export { add, sub, otherProperty }
等价于
// 导入的所有文件会统一被导出 export { add, sub } from './math.js' export {otherProperty} from './other.js'
等价于
export * from './math.js' export * from './other.js'
为什么要这样做呢?
在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中。 这样方便指定统一的接口规范,也方便阅读。这个时候,我们就可以使用export和import结合使用。
前面我们学习的导出功能都是有名字的导出(named exports):
在导出export时指定了名字。
在导入import时需要知道具体的名字。
还有一种导出叫做默认导出(default export)
// foo.js const name = "zh" cconst age = 22 export { name, // 或者这样的默认导出 // age as default } export default age
// 导入语句: 导入的默认的导出 import foo, {name} from './foo.js' console.log(foo, name) // 22 zh
默认导出export时可以不需要指定名字。
在导入时不需要使用 {},并且可以自己来指定名字。
它也方便我们和现有的CommonJS等规范相互操作。
注意:在一个模块中,只能有一个默认导出(default export)。
通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:
if(true) { import foo from './foo.js' }
为什么会出现这个情况呢?
这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系。
由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况。
但是某些情况下,我们确确实实希望动态的来加载某一个模块:
如果根据不同的条件,动态来选择加载模块的路径。
这个时候我们需要使用 import() 函数来动态加载。import函数返回的结果是一个Promise。
import("./foo.js").then(res => { console.log("res:", res.name) })
es11新增了一个属性。meta属性本身也是一个对象: { url: "当前模块所在的路径" }
console.log(import.meta)
ES Module是如何被浏览器解析并且让模块之间可以相互引用的呢?
ES Module的解析过程可以划分为三个阶段:
阶段一:构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record)。
阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。
阶段三:运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中。
Fasa 1:
Fasa 2 dan 3:
Oleh itu, dapat dilihat daripada perkara di atas bahawa mengubah suai nilai pembolehubah dalam fail eksport akan mempengaruhi nilai dalam fail import. Dan fail yang diimport adalah terhad daripada mengubah suai nilai fail yang dieksport.
Cadangan berkaitan: Tutorial pembelajaran javascript
Atas ialah kandungan terperinci Modularisasi sintaks JavaScript lanjutan (koleksi disyorkan). Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!