整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

(1)响应式内页布局分析HTML模块化和CSS模块化详解

页设计图

视频讲解课程

1.设计图分析和HTML模块化结构:https://www.ixigua.com/i6903745684596326915/

2.HTML模块化和CSS模块化示例:https://www.ixigua.com/i6904203127541465611/


HTML+CSS模块化基础代码

核心知识点

  1. 分析设计图结构和模块划分
    1. 横向分:头部,导航,banner,中间主体,底部
    2. 按页面分:列表页,详情页,单页
    3. 模块划分:列表,轮播,自定义模块
  2. 了解页面布局和模块化布局的区别
    1. 页面布局主要是分栏唯一功能,不能定义样式,一般用 col-为前缀命名
  3. 了解html模块化命名规范
    1. 列表类模块使用 ul-为前缀命名
      1. 列表li里面的命名规范
        1. 都以短的英文缩写为主
        2. 图片 .pic
        3. 文本 .txt
        4. 标题 .tit
        5. 日期 .date
        6. 简介 .desc
        7. 信息 .info
        8. 按钮 .btn
        9. 更多 .more
    2. 其他自定义模块都用 m- 为前缀命名
    3. 单独元素都已 g- 为前缀命名
      1. 大标题 g-tit1
      2. 按钮 g-btn
    4. 通用的模块统一命名
      1. 头部 header
      2. 底部 footer
      3. 主导航 nav
      4. 侧导航 snv
      5. 分页 pages
      6. 当前位置 cur
  4. 了解CSS模块化写法规范和编码顺序
    1. 先写初始化样式
    2. 然后是头部底部公用样式
    3. 然后写每个模块的样式
    4. 每个模块的样式都以模块命开头,每个模块独立
    5. .wp是限制页面宽度的
    6. .col- 是页面布局分栏的

随着 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)来声明依赖以及导出数据,这与当下的模块化方案并无巨大的差异,可本质上却有千差万别,无法满足的一些重要的特性;

  • 定义模块时,声明的依赖不是强制自动引入的,即在定义该模块之前,必须手动引入依赖的模块代码;
  • 定义模块时,其代码就已经完成执行过程,无法实现按需加载;
  • 跨文件使用模块时,需要将模块挂载到全局变量(window)上;

AMD & CMD 二分天下

题外话:由于年代久远,这两种模块化方案逐渐淡出历史舞台,具体特性不再细聊;

为了解决”刀耕火种”时代存留的需求,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 = {};
});

CommonJS

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;

规范

  • module (Object): 模块本身
  • exports (*): 模块的导出部分,即暴露出来的内容
  • require (Function): 加载模块的函数,获得目标模块的导出值(基础类型为复制,引用类型为浅拷贝),可以加载内置模块、npm 模块和自定义模块

实现

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));
});

特性总结

  • 同步执行模块声明和引入逻辑,分析一些复杂的依赖引用(如循环依赖)时需注意;
  • 缓存机制,性能更优,同时限制了内存占用;
  • Module 模块可供改造的灵活度高,可以实现一些定制需求(如热更新、任意文件类型模块支持);

ES Module(推荐使用)

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;

主要差异在于:

  • ES Module 会对静态代码分析,即在代码编译时进行模块的加载,在运行时之前就已经确定了依赖关系(可解决循环引用的问题);
  • ES Module 关键字:import export 以及独有的 default 关键字,确定默认的导出值;
  • ES Module 中导出的值是一个 只读的值的引用 ,无论基础类型和复杂类型,而在 CommonJS 中 require 的是值的拷贝,其中复杂类型是值的浅拷贝;
// 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

UMD

通过一层自执行函数来兼容各种模块化规范的写法,兼容 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 / 图片等文件也能为模块化所使用;

附录1:特性一览表

附录2:参考

  • AMD 模块化规范: https://github.com/amdjs/amdjs-api/wiki/AMD
  • CMD 模块定义规范:https://github.com/seajs/seajs/issues/242
  • webpack 模块相关文档: https://webpack.js.org/concepts/modules/
  • 浏览器加载 CommonJS 模块的原理与实现:http://www.ruanyifeng.com/blog/2015/05/commonjs-in-browser.html

作者:x-cold

本文主要理理js模块化相关知识。
涉及到内联脚本、外联脚本、动态脚本、阻塞、
deferasyncCommonJSAMDCMDUMDES 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>

但这不是最好的办法,我们接着往下看。

defer

我们给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 特性。

async

我们可以给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模块化,我们接着往下看。

CommonJS

很长一段时间 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

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

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

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 {};
}));

ES Module

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 文件。

Vite

在浏览器支持 ES Module 之前,我们用工具实现JavaScript模块化的开发,比如webpack、Rollup 和 Parcel 。但是当项目越来越大后,本地热更新越来越慢,而 Vite 旨在利用ESM解决上述问题。

Vite使用简单,可以去官网(https://cn.vitejs.dev/)看看。

总结

老的规范了解即可,未来是ES Module的,用Vite可以极大的提升开发时的体验,生产环境用Rollup打包。