页设计图
1.设计图分析和HTML模块化结构:https://www.ixigua.com/i6903745684596326915/
2.HTML模块化和CSS模块化示例:https://www.ixigua.com/i6904203127541465611/
核心知识点
随着 Web 技术的蓬勃发展和依赖的基础设施日益完善,前端领域逐渐从浏览器扩展至服务端(Node.js),桌面端(PC、Android、iOS),乃至于物联网设备(IoT),其中 JavaScript 承载着这些应用程序的核心部分,随着其规模化和复杂度的成倍增长,其软件工程体系也随之建立起来(协同开发、单元测试、需求和缺陷管理等),模块化编程的需求日益迫切。
JavaScript 对模块化编程的支持尚未形成规范,难以堪此重任;一时间,江湖侠士挺身而出,一路披荆斩棘,从刀耕火种过渡到面向未来的模块化方案;
更多关于我的文章和项目,欢迎关注 @x-cold
模块化编程就是通过组合一些__相对独立可复用的模块__来进行功能的实现,其最核心的两部分是__定义模块__和__引入模块__;
尽管 JavaScript 语言层面并未提供模块化的解决方案,但利用其可__面向对象__的语言特性,外加__设计模式__加持,能够实现一些简单的模块化的架构;经典的一个案例是利用单例模式模式去实现模块化,可以对模块进行较好的封装,只暴露部分信息给需要使用模块的地方;
// Define a module var moduleA = (function ($, doc) { var methodA = function() {}; var dataA = {}; return { methodA: methodA, dataA: dataA }; })(jQuery, document); // Use a module var result = moduleA.mehodA();
直观来看,通过立即执行函数(IIFE)来声明依赖以及导出数据,这与当下的模块化方案并无巨大的差异,可本质上却有千差万别,无法满足的一些重要的特性;
题外话:由于年代久远,这两种模块化方案逐渐淡出历史舞台,具体特性不再细聊;
为了解决”刀耕火种”时代存留的需求,AMD 和 CMD 模块化规范问世,解决了在浏览器端的异步模块化编程的需求,__其最核心的原理是通过动态加载 script 和事件监听的方式来异步加载模块;__
AMD 和 CMD 最具代表的两个作品分别对应 require.js 和 sea.js;其主要区别在于依赖声明和依赖加载的时机,其中 require.js 默认在声明时执行, sea.js 推崇懒加载和按需使用;另外值得一提的是,CMD 规范的写法和 CommonJS 极为相近,只需稍作修改,就能在 CommonJS 中使用。参考下面的 Case 更有助于理解;
// AMD define(['./a','./b'], function (moduleA, moduleB) { // 依赖前置 moduleA.mehodA(); console.log(moduleB.dataB); // 导出数据 return {}; }); // CMD define(function (requie, exports, module) { // 依赖就近 var moduleA = require('./a'); moduleA.mehodA(); // 按需加载 if (needModuleB) { var moduleB = requie('./b'); moduleB.methodB(); } // 导出数据 exports = {}; });
2009 年 ry 发布 Node.js 的第一个版本,CommonJS 作为其中最核心的特性之一,适用于服务端下的场景;历年来的考察和时间的洗礼,以及前端工程化对其的充分支持,CommonJS 被广泛运用于 Node.js 和浏览器;
// Core Module const cp = require('child_process'); // Npm Module const axios = require('axios'); // Custom Module const foo = require('./foo'); module.exports = { axios }; exports.foo = foo;
1、模块定义
默认任意 .node .js .json 文件都是符合规范的模块;
2、引入模块
首先从缓存(require.cache)优先读取模块,如果未命中缓存,则进行路径分析,然后按照不同类型的模块处理:
其中在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装,结果如下:
(function (exports, require, module, __filename, __dirname) { var circle = require('./circle.js'); console.log('The area of a circle of radius 4 is ' + circle.area(4)); });
ES Module 是语言层面的模块化方案,由 ES 2015 提出,其规范与 CommonJS 比之 ,导出的值都可以看成是一个具备多个属性或者方法的对象,可以实现互相兼容;但写法上 ES Module 更简洁,与 Python 接近;
import fs from 'fs'; import color from 'color'; import service, { getArticles } from '../service'; export default service; export const getArticles = getArticles;
主要差异在于:
// a.js export let a = 1; export function caculate() { a++; }; // b.js import { a, caculate } from 'a.js'; console.log(a); // 1 caculate(); console.log(a); // 2 a = 2; // Syntax Error: "a" is read-only
通过一层自执行函数来兼容各种模块化规范的写法,兼容 AMD / CMD / CommonJS 等模块化规范,贴上代码胜过千言万语,需要特别注意的是 ES Module 由于会对静态代码进行分析,故这种运行时的方案无法使用,此时通过 CommonJS 进行兼容;
(function (global, factory) { if (typeof exports === 'object') { module.exports = factory(); } else if (typeof define === 'function' && define.amd) { define(factory); } else { this.eventUtil = factory(); } })(this, function (exports) { // Define Module Object.defineProperty(exports, "__esModule", { value: true }); exports.default = 42; });
为了在浏览器环境中运行模块化的代码,需要借助一些模块化打包的工具进行打包( 以 webpack 为例),定义了项目入口之后,会先快速地进行依赖的分析,然后将所有依赖的模块转换成浏览器兼容的对应模块化规范的实现;
从上面的介绍中,我们已经对其规范和实现有了一定的了解;在浏览器中,要实现 CommonJS 规范,只需要实现 module / exports / require / global 这几个属性,由于浏览器中是无法访问文件系统的,因此 require 过程中的文件定位需要改造为加载对应的 JS 片段(webpack 采用的方式为通过函数传参实现依赖的引入)。具体实现可以参考:tiny-browser-require。
webpack 打包出来的代码快照如下,注意看注释中的时序;
(function (modules) { // The module cache var installedModules = {}; // The require function function __webpack_require__(moduleId) {} return __webpack_require__(0); // ---> 0 }) ({ 0: function (module, exports, __webpack_require__) { // Define module A var moduleB = __webpack_require__(1); // ---> 1 }, 1: function (module, exports, __webpack_require__) { // Define module B exports = {}; // ---> 2 } });
实际上,ES Module 的处理同 CommonJS 相差无几,只是在定义模块和引入模块时会去处理 __esModule 标识,从而兼容其在语法上的差异。
1、浏览器环境下,网络资源受到较大的限制,因此打包出来的文件如果体积巨大,对页面性能的损耗极大,因此需要对构建的目标文件进行拆分,同时模块也需要支持动态加载;
webpack 提供了两个方法 require.ensure() 和 import() (推荐使用)进行模块的动态加载,至于其中的原理,跟上面提及的 AMD & CMD 所见略同,import() 执行后返回一个 Promise 对象,其中所做的工作无非也是动态新增 script 标签,然后通过 onload / onerror 事件进一步处理。
2、由于 require 函数是完全自定义的,我们可以在模块化中实现更多的特性,比如通过修改 require.resolve 或 Module._extensions 扩展支持的文件类型,使得 css / .jsx / .vue / 图片等文件也能为模块化所使用;
作者:x-cold
本文主要理理js模块化相关知识。
涉及到内联脚本、外联脚本、动态脚本、阻塞、defer、async、CommonJS、AMD、CMD、UMD、ES Module。顺带探究下Vite。
假设你是一个前端新手,现在入门,那么我们创建一个html页面,需要新建一个index.html文件:
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<p id="content">hello world</p>
</body>
</html>
如果需要在页面中执行javascript代码,我们就需要在 HTML 页面中插入 <script> 标签。
有2种插入方式:
1、放在<head>中
2、放在<body>中
比如,点击hello world之后,在hello world后面加3个感叹号的功能,我们在head中加入script标签,并给hello world绑定点击事件:
<!DOCTYPE html>
<html>
<head>
<title>test</title>
<script>
function myFunction() {
document.getElementById('content').innerHTML = 'hello world!!!'
}
</script>
</head>
<body>
<p id="content" onclick="myFunction()">hello world</p>
</body>
</html>
如果加在body中,一般放在body的最后面:
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<p id="content" onclick="myFunction()">hello world</p>
<script>
function myFunction() {
document.getElementById('content').innerHTML = 'hello world!!!'
}
</script>
</body>
</html>
简单的逻辑我们可以用这2种方式写,这种方式叫做内联脚本。
当逻辑复杂时,我们可以把上面的script标签中的代码抽取出来,比如在html的同级目录创建一个js文件夹,里面新建一个a.js的文件。
a.js中写上面script标签中的代码:
function myFunction() {
document.getElementById('content').innerHTML = 'hello world!!!'
}
上面的script标签则可以改成:
<script src="./js/a.js"></script>
上面的2种写法,浏览器在加载html时,遇到script标签,会停止解析html。
内联脚本会立刻执行;外联脚本会先下载再立刻执行。
等脚本执行完毕才会继续解析html。
(html解析到哪里,页面就能显示到哪里,用户也能看到哪里)
比如下面的代码:
<p>...content before script...</p>
<script src="./js/a.js"></script>
<p>...content after script...</p>
解析到第一个p标签,我们能看到...content before script...显示在了页面中,然后浏览器遇到script标签,会停止解析html,而去下载a.js并执行,执行完a.js才会继续解析html,然后页面中才会出现...content after script...。
我们可以通过Chrome的Developer Tools分析一下index.html加载的时间线:
这会导致2个问题:
1、脚本无法访问它下面的dom;
2、如果页面顶部有个笨重的脚本,在它执行完之前,用户都看不到完整的页面。
对于问题2,我们可以把脚本放在页面底部,这样它可以访问到上面的dom,且不会阻塞页面的显示:
<body>
...all content is above the script...
<script src="./js/a.js"></script>
</body>
但这不是最好的办法,我们接着往下看。
我们给script标签加defer属性,就像下面这样:
<p>...content before script...</p>
<script defer src="./js/a.js"></script>
<p>...content after script...</p>
defer 特性告诉浏览器不要等待脚本。于是,浏览器将继续解析html,脚本会并行下载,然后等 DOM 构建完成后,脚本才会执行。
这样script标签不再阻塞html的解析。
这时再看时间线:
需要注意的是,具有 defer 特性的脚本保持其相对顺序。
比如:
<script defer src="./js/a.js"></script>
<script defer src="./js/b.js"></script>
上面的2个脚本会并行下载,但是不论哪个先下载完成,都是先执行a.js,a.js执行完才会执行b.js。
这时,如果b.js依赖a.js,这种写法将很有用。
另外需要注意的是,defer 特性仅适用于外联脚本,即如果 script标签没有 src属性,则会忽略 defer 特性。
我们可以给script标签加async属性,就像下面这样:
<script async src="./js/a.js"></script>
这会告诉浏览器,该脚本完全独立。
独立的意思是,DOM 和其他脚本不会等待它,它也不会等待其它东西。async 脚本就是一个会在加载完成时立即执行的完全独立的脚本。
这时再看时间线:
可以看到,虽然下载a.js不阻塞html的解析,但是执行a.js会阻塞。
还需要注意多个async时的执行顺序,比如下面这段代码:
<p>...content before script...</p>
<script async src="./js/a.js"></script>
<script async src="./js/b.js"></script>
<p>...content after script...</p>
两个p标签的内容会立刻显示出来,a.js和b.js则并行下载,且下载成功后立刻执行,所以多个async时的执行顺序是谁先下载成功谁先执行。
一些比较独立的脚本,比如性能监控,就很适合用这种方式加载。
另外,和defer一样,async 特性也仅适用于外联脚本。
我们可以动态地创建一个script标签并append到文档中。
let script = document.createElement('script')
script.src = '/js/a.js'
document.body.append(script)
append后脚本就会立刻开始加载,表现默认和加了async属性一致。
我们可以显示的设置script.async = false来改变这个默认行为,那么这时表现就和加了defer属性一致。
上面的这些写法,当script标签变多时,容易导致全局作用域污染,还要维护书写顺序,要解决这个问题,需要一种将 JavaScript 程序拆分为可按需导入的单独模块的机制,即js模块化,我们接着往下看。
很长一段时间 JavaScript 没有模块化的概念,直到 Node.js 的诞生,把 JavaScript 带到服务端,这时,CommonJS诞生了。
CommonJS定义了三个全局变量:
require,exports,module
require 读入并执行一个 js 文件,然后返回其 exports 对象;
exports 对外暴露模块的接口,可以是任何类型,指向 module.exports;
module 是当前模块,exports 是 module 上的一个属性。
Node.js 使用了CommonJS规范。
比如:
// a.js
let name = 'Lily'
export.name = name
// b.js
let a = require('a.js')
console.log(a.name) // Lily
由于CommonJS不适合浏览器端,于是出现了AMD和CMD规范。
AMD(Asynchronous Module Definition) 是 RequireJS 在推广过程中对模块定义的规范化产出。
基本思想是,通过 define 方法,将代码定义为模块。当这个模块被 require 时,开始加载依赖的模块,当所有依赖的模块加载完成后,开始执行回调函数,返回该模块导出的值。
使用时,需要先引入require.js:
<script src="require.js"></script>
<script src="a.js"></script>
然后可以这样写:
// a.js
define(function() {
let name = 'Lily'
return {
name
}
})
// b.js
define(['a.js'], function(a) {
let name = 'Bob'
console.log(a.name) // Lily
return {
name
}
})
CMD(Common Module Definition) 是 Sea.js 在推广过程中对模块定义的规范化产出。
使用时,需要先引入sea.js:
<script src="sea.js"></script>
<script src="a.js"></script>
然后可以这样写:
// a.js
define(function(require, exports, module) {
var name = 'Lily'
exports.name = name
})
// b.js
define(function(require, exports, module) {
var name = 'Bob'
var a = require('a.js')
console.log(a.name) // 'Lily'
exports.name = name
})
UMD (Universal Module Definition) 目的是提供一个前后端跨平台的解决方案(兼容全局变量、AMD、CMD和CommonJS)。
实现很简单,判断不同的环境,然后以不同的方式导出模块:
(function (root, factory) {
if (typeof define === 'function' && (define.amd || define.cmd)) {
// AMD、CMD
define([], factory);
} else if (typeof module !== 'undefined' && typeof exports === 'object') {
// Node、CommonJS
module.exports = factory();
} else {
// 浏览器全局变量
root.moduleName = factory();
}
}(this, function () {
// 只需要返回一个值作为模块的export
// 这里我们返回了一个空对象
// 你也可以返回一个函数
return {};
}));
AMD 和 CMD 是社区的开发者们制定的模块加载方案,并不是语言层面的标准。从 ES6 开始,在语言标准的层面上,实现了模块化功能,而且实现得相当简单,完全可以取代上文的规范,成为浏览器和服务器通用的模块解决方案。
ES6 的模块自动采用严格模式。模块功能主要由两个命令构成:export和import。
export命令用于规定模块的对外接口;
import命令用于输入其他模块提供的功能。
比如上面的代码,我们可以这样写:
// a.js
const name = 'Lily'
export {
name
}
// 等价于
export const name = 'Lily'
// b.js
import { name } from 'a.js'
console.log(name) // Lily
// b.js
import * as a from 'a.js'
console.log(a.name) // Lily
此外,还可以用export default默认导出的写法:
// a.js
const name = 'Lily'
export default {
name
}
// b.js
import a from 'a.js'
console.log(a.name) // Lily
如果只想运行a.js,可以只import:
// b.js
import 'a.js'
我们可以给script标签加type=module让浏览器以 ES Module 的方式加载脚本:
<script type="module" src="./js/b.js"></script>
这时,script标签会默认有defer属性(也可以设置成async),支持内联和外联脚本。
这时我们运行打开index.html,会发现浏览器报错了:
这是因为 type=module 的 script 标签加强了安全策略,浏览器加载不同域的脚本资源时,如果服务器未返回有效的 Allow-Origin 相关 CORS 头,会禁止加载改脚本。而这里启动的index.html是一个本地文件(地址是file://路径),将会遇到 CORS 错误,需要通过一个服务器来启动 HTML 文件。
在浏览器支持 ES Module 之前,我们用工具实现JavaScript模块化的开发,比如webpack、Rollup 和 Parcel 。但是当项目越来越大后,本地热更新越来越慢,而 Vite 旨在利用ESM解决上述问题。
Vite使用简单,可以去官网(https://cn.vitejs.dev/)看看。
老的规范了解即可,未来是ES Module的,用Vite可以极大的提升开发时的体验,生产环境用Rollup打包。
*请认真填写需求信息,我们会在24小时内与您取得联系。