12 前端工程化

组件化


1. 组件化理解

就是将页面的某一部分独立出来,将这一部分的数据层(M)、视图层(V)和控制层(C)用黑盒的形式全部封装到一个组件内,暴露出一些开箱即用的函数和属性供外部调用。无论这个组件放到哪里去使用,它都具有一样的功能和样式,从而实现复用(只写一处,处处复用 ),这种整体化的思想就是组件化
每个组件都是独立的个体,都只负责一块功能。组件之间互相独立,通过特定的方式进行沟通。外部完全不用考虑组件的内部实现逻辑。一个好的前端组件,必须要把维护性,复用性,扩展性,性能做到极致

2. 组件化与模块化的区别

2.1 从历史发展角度来讲

随着前端开发越来越复杂、对效率要求越来越高,由项目级模块化开发,进一步提升到通用功能组件化开发,模块化是组件化的前提,组件化是模块化的演进

2.2 从整体概念来讲

  • 模块化是一种分治的思想,述求是解耦,一般指的是 JavaScript 模块,比如用来格式化时间的模块
  • 组件化是模块化思想的实现手段,述求是复用,包含了 template,style,script,script又可以由各种模块组成

2.3 从复用的角度来讲

  • 模块一般是项目范围内按照项目业务内容来划分的,比如一个项目划分为子系统、模块、子模块,代码分开就是模块,位于架构业务框架层,横向分块
  • 组件是按照一些小功能的通用性和可复用性抽象出来的,可以跨项目的,是可复用的模块,通常位于架构底层,被其他层所依赖

2.4 从划分的角度来讲

  • 模块是从代码逻辑的角度进行划分,方便代码分层开发,保证每个功能模块的职能单一
  • 组件是从 UI 界面的角度进行划分,前端的组件化,方便 UI 组件的重用

3. 为什么要前端组件化

随着前端项目复杂度的急剧增加,我们很容易遇到以下这些场景:

  • 页面逻辑越来越多,代码越写越庞大,容易牵一发而动全身
  • 同样的逻辑在多个地方重复编写,改一个问题要在多个地方进行同样的修改

以上场景带来的问题就是:

  • 项目复杂度增加
  • 重复性劳动多,效率低
  • 代码质量差,不可控

因此前端组件化可以给我们带来:

  • 增加代码的复用性,灵活性
  • 提高开发效率,降低开发成本
  • 便于各个开发者之间分工协作、同步开发
  • 降低系统各个功能的耦合性,提高了功能内部的聚合性
  • 降低代码的维护成本

4. 组件的划分

4.1 划分方法

尽可能抽象和解耦。不断抽象出一个跟业务没有关系的模块,它是可以继承的,这就是组件化设计的思维转换。

划分粒度:需要根据实际情况权衡,太小会提升维护成本,太大又不够灵活。

目前还没有一套原则和方法论来指导组件的划分,我们只能根据前人的经验再结合实际情况来进行组件的划分。

关于组件划分的一些建议:

  • 组件之间的依赖应该尽可能的少。
  • 单个组件代码量最好不要超过1000行。
  • 组件划分的依据通常是业务逻辑、功能,要考虑各组件之间的关系是否明确,以及组件的可复用度。
  • 每一个组件都应该有其独特的划分目的,有的是为了复用实现,有的是为了封装复杂度、清晰业务实现。

我经常的做法是:如果看到有多个页面都出现了这个重复元素,则抽取成一个组件。还有在开发之中发现结构相似的也可以考虑抽取成一个组件。没有必要在一开始就把所有都抽取成一个个组件。

4.2 组件分类

4.2.1 基础UI组件

这是最小化的组件,它们不依赖于其他组件。作为页面中最少的元素而存在,比如按钮、下拉菜单、对话框等。其中大部分是对原生 Web 元素的封装,例如: 、 、 ,它们以简单的形式存在。

  • 在创建基础组件的过程中,要遵循一个基本原则:基础组件是独立存在的。它们可以共享配置,但是不能相互依赖,依赖意味着它不是基础组件。
  • 像 antd、iview、element-ui 里提供的基本都是基础 UI 组件。
4.2.2 复合组件

复合组件是在多个基础的 UI 组件上的进一步结合。大部分复合组件,包含了一些复杂的组件,往往需要花很长的时间,才能变成一个可稳定使用的版本。复合组件包含以下几个部分:

  • 表格:表格往往带有复杂的交互,比如固定行、固定列、可编辑、虚拟滚动等。由于其数据量大,往往又对性能有很高的要求。
  • 图表:图表的门槛相对比较高,并且种类繁多,对于显示、交互的要求也高。
  • 富文本编辑器:几乎是最复杂的组件,其功能需求往往与 Word 进行对比,其代码量可能接近 Word 的数量级。
4.2.3 业务组件

业务组件是我们在实现业务功能的过程中抽象出来的组件,其作用是在应用中复用业务逻辑。当它们涉及一些更复杂的业务情形时,就要考虑是否将这些组件放入组件库中。

  • 通常是根据最小业务状态抽象而出,有些业务组件也具有一定的复用性,但大多数是一次性组件。
  • 特点:UI可配置,业务逻辑完整。有完整的后台流程,数据结构。

5. 组件的设计原则

  1. 标准性:任何一个组件都应该遵守一套标准,可以使得不同区域的开发人员据此标准开发出一套标准统一的组件。
  2. 单一职责原则:一个组件只专注做一件事,且把这件事做好。一个功能如果可以拆分成多个功能点,那就可以将每个功能点封装成一个组件,当然也不是组件的颗粒度越小越好,只要将一个组件内的功能逻辑控制在一个可控的范围内即可。
  3. 开闭原则:对扩展开放,对修改关闭。属性配置等 API 对外开放,组件内部状态对外封闭。
  4. 追求短小精悍
  5. 避免太多参数扁平化参数:除了数据,避免复杂的对象,尽量只接收原始类型的值。
  6. 合理的依赖关系:父组件不依赖子组件,删除某个子组件不会造成功能异常
  7. 适用SPOT(Single Point of Truth)法则:尽量不要重复代码
  8. 追求无副作用
  9. 复用与易用
  10. 避免暴露组件内部实现
  11. 入口处检查参数的有效性,出口处检查返回的正确性
  12. 稳定抽象原则(SAP)
  • 组件的抽象程度与其稳定程度成正比
  • 一个稳定的组件应该是抽象的(逻辑无关的)
  • 一个不稳定的组件应该是具体的(逻辑相关的)
  • 为降低组件之间的耦合度,我们要针对抽象组件编程,而不是针对业务实现编程
  1. 良好的接口设计,API 尽量和已知概念保持一致

模块化


1. 模块化理解

1.1 模块化简介

模块化,就是把一个个文件看成一个模块,它们之间作用域相互隔离,互不干扰。一个模块就是一个功能,它们可以被多次复用。另外,模块化的设计也体现了分治的思想。什么是分治?维基百科 (opens new window)的定义如下:
字面上的解释是"分而治之",就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
  • 模块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

1.2 模块化进程

全局function模式:将不同的功能封装成不同的全局函数
  • 编码: 将不同的功能封装成不同的全局函数
  • 问题: 污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系
javascript 复制代码
function m1(){
  //...
}
function m2(){
  //...
}
namespace模式:简单对象封装
  • 作用: 减少了全局变量,解决命名冲突
  • 问题: 数据不安全(外部可以直接修改模块内部的数据)
javascript 复制代码
let myModule = {
  data: 'www.baidu.com',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}
myModule.data = 'other data' //能直接修改模块内部的数据
myModule.foo() // foo() other data
IIFE模式:匿名函数自调用(闭包)
  • 作用: 数据是私有的, 外部只能通过暴露的方法操作
  • 编码: 将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口
  • 问题: 如果当前这个模块依赖另一个模块怎么办?
json 复制代码
// index.html文件
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
    myModule.foo()
    myModule.bar()
    console.log(myModule.data) //undefined 不能访问模块内部数据
    myModule.data = 'xxxx' //不是修改的模块内部的data
    myModule.foo() //没有改变
</script>
// module.js文件
(function(window) {
  let data = 'www.baidu.com'
  //操作数据的函数
  function foo() {
    //用于暴露有函数
    console.log(`foo() ${data}`)
  }
  function bar() {
    //用于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露行为
  window.myModule = { foo, bar } //ES6写法
})(window)
IIFE模式增强:引入依赖
javascript 复制代码
// module.js文件
(function(window, $) {
  let data = 'www.baidu.com'
  //操作数据的函数
  function foo() {
    //用于暴露有函数
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }
  function bar() {
    //用于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露行为
  window.myModule = { foo, bar }
})(window, jQuery)
// index.html文件
  <!-- 引入的js必须有一定顺序 -->
  <script type="text/javascript" src="jquery-1.10.1.js"></script>
  <script type="text/javascript" src="module.js"></script>
  <script type="text/javascript">
    myModule.foo()
  </script>
  • 通过jquery方法将页面的背景颜色改成红色,所以必须先引入jQuery库,就把这个库当作参数传入。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显

1.3 模块化优点

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

1.4 引入多个 <script> 的问题

  • 请求过多首先我们要依赖多个模块,那样就会发送多个请求,导致请求过多
  • 依赖模糊我们不知道他们的具体依赖关系是什么,也就是说很容易因为不了解他们之间的依赖关系导致加载先后顺序出错
  • 难以维护以上两种原因就导致了很难维护,很可能出现牵一发而动全身的情况导致项目出现严重的问题。 模块化固然有多个好处,然而一个页面需要引入多个js文件,就会出现以上这些问题。而这些问题可以通过模块化规范来解决

2. 服务端模块化

2.1 Nodejs和CommonJS的关系

  • Nodejs的模块化能一种成熟的姿态出现离不开CommonJS的规范的影响
  • 在服务器端CommonJS能以一种寻常的姿态写进各个公司的项目代码中,离不开Node的优异表现
  • Node并非完全按照规范实现,针对模块规范进行了一定的取舍,同时也增加了少许自身特性

2.2 CommonJS规范简介

CommonJS对模块的定义非常简单,主要分为模块引用,模块定义和模块标识3部分

2.2.1 模块引用
javascript 复制代码
var add = require('./add.js');
var config = require('config.js');
var http = require('http');
2.2.2 模块定义
javascript 复制代码
module.exports.add = function () {
  ...
}
module.exports = function () {
  return ...
}

可以在一个文件中引入模块并导出另一个模块

javascript 复制代码
var add = require('./add.js');
module.exports.increment = function () {
  return add(val, 1);
}
2.2.3 模块标识

模块标识就是require( )函数的参数,规范是这样的:

  • 必须是字符串
  • 可以是以./ .../开头的相对路径
  • 可以是绝对路径
  • 可以省略后缀名

CommonJS的模块规范定义比较简单,意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出将上下游模块无缝衔接,每个模块具有独立的空间,它们互不干扰。

2.3 Nodejs的模块化实现

Node模块在实现中并非完全按照CommonJS来,进行了取舍,增加了一些自身的的特性。

Node中一个文件是一个模块------module

一个模块就是一个Module的实例

javascript 复制代码
// Node中Module构造函数:
function Module(id, parent){
  this.id = id;
  this.exports = {};
  this.parent = parent;
  if(parent && parent.children) {
    parent.children.push(this);
  }
  this.filename = null;
  this.loaded = false;
  this.children = [];
}

//实例化一个模块
 var module = new Module(filename, parent);

其中id 是模块id,exports是这个模块要暴露出来的api,parent是父级模块,loaded表示这个模块是否加载完成,因为CommonJS是运行时加载,loaded表示文件是否已经执行完毕返回一个对象。

2.4 Node模块分类

2.4.1 核心模块

就是Node内置的模块比如http, path等。在Node的源码的编译时,核心模块就一起被编译进了二进制执行文件,部分核心模块(内建模块)被直接加载进内存中。

在Node模块的引入过程中,一般要经过一下三个步骤

  • 路径分析
  • 文件定位
  • 编译执行

核心模块会省略文件定位编译执行 这两步,并且在路径分析中会优先判断,加载速度比一般模块更快。

2.4.2 文件模块

就是外部引入的模块如node_modules里通过npm安装的模块,或者我们项目工程里自己写的一个js文件或者json文件。

文件模块引入过程以上三个步骤都要经历

2.5 那么NodeJS require的时候是怎么路径分析,文件定位并且编译执行的?

2.5.1 路径分析

前面已经说过,不论核心模块还是文件模块都需要经历路径分析这一步,当我们require一个模块的时候,Node是怎么区分是核心模块还是文件模块,并且进行查找定位呢?

Node支持如下几种形式的模块标识符,来引入模块:

javascript 复制代码
//核心模块
require('http')

----------------------------
//文件模块

//以.开头的相对路径,(可以不带扩展名)
require('./a.js')

  
//以..开头的相对路径,(可以不带扩展名)
require('../b.js')


//以/开始的绝对路径,(可以不带扩展名)
require('/c.js')


//外部模块名称
require('express')

//外部模块某一个文件
require('codemirror/addon/merge/merge.js');

那么对于这个都是字符串的引入方式,

  • Node 会优先去内存中查找匹配核心模块,如果匹配成功便不会再继续查找

(1)比如require http 模块的时候,会优先从核心模块里去成功匹配

  • 如果核心模块没有匹配成功,便归类为文件模块

(2) 以.、...和/开头的标识符,require都会根据当前文件路径将这个相对路径或者绝对路径转化为真实路径,也就是我们平时最常见的一种路径解析

(3)非路径形式的文件模块 如上面的'express' 和'codemirror/addon/merge/merge.js',这种模块是一种特殊的文件模块,一般称为自定义模块。

自定义模块的查找最费时,因为对于自定义模块有一个模块路径,Node会根据这个模块路径依次递归查找。
模块路径 ------Node的模块路径是一个数组,模块路径存放在module.paths属性上。

我们可以找一个基于npm或者yarn管理项目,在根目录下创建一个test.js文件,内容为console.log(module.paths),如下:

javascript 复制代码
//test.js
console.log(module.paths);

然后在根目录下用Node执行

javascript 复制代码
node test.js

可以看到模块路径的生成规则如下:

  • 当前路文件下的node_modules目录
  • 父目录下的node_modules目录
  • 父目录的父目录下的node_modules目录
  • 沿路径向上逐级递归,直到根目录下的node_modules目录

对于自定义文件比如express,就会根据模块路径依次递归查找。

在查找同时并进行文件定位。

2.5.2 文件定位
  • 扩展名分析

我们在使用require的时候有时候会省略扩展名,那么Node怎么定位到具体的文件呢?

这种情况下,Node会依次按照.js、.json、.node的次序一次匹配。(.node是C++扩展文件编译之后生成的文件)

若扩展名匹配失败,则会将其当成一个包来处理,我这里直接理解为npm包

  • 包处理

对于包Node会首先在当前包目录下查找package.json(CommonJS包规范)通过JSON.parse( )解析出包描述对象,根据main属性指定的入口文件名进行下一步定位。

如果文件缺少扩展名,将根据扩展名分析规则定位。

若main指定文件名错误或者压根没有package.json,Node会将包目录下的index当做默认文件名。

再依次匹配index.js、index.json、index.node。

若以上步骤都没有定位成功将,进入下一个模块路径------父目录下的node_modules目录下查找,直到查找到根目录下的node_modules,若都没有定位到,将抛出查找失败的异常。

2.5.3 模块编译
  • .js文件------通过fs模块同步读取文件后编译执行
  • .node文件------用C/C++编写的扩展文件,通过dlopen( )方法加载最后编译生成的文件。
  • .json------通过fs模块同步读取文件后,用JSON.parse( ) 解析返回结果。
  • 其余扩展名文件。它们都是被当做.js文件载入。

每一个编译成功的文件都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能。

这里我们只讲解一下JavaScript模块的编译过程,以解答前面所说的CommonJS模块中的require、exports、module变量的来源。

我们还知道Node的每个模块中都有___filename、_dirname 这两个变量,是怎么来的的呢?

其实JavaScript模块在编译过程中,Node对获取的JavaScript文件内容进行了头部和尾部的包装。在头部添加了(function (exports, require, module,__filename, __dirname){\n,而在尾部添加了\n}); 。

因此一个JS模块经过编译之后会被包装成下面的样子:

javascript 复制代码
(function(exports, require, module, __filename, __dirname){
  var express = require('express') ;
  exports.method = function (params){
   ...
  };
});

2.6 CommonJS 详解

Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。加载某个模块,实质上是加载module.exports属性。就是把另一个js文件的变量或者函数,作为一个对象,引入了当前js文件。在CommonJS的这种方式下,每次require时相当于深拷贝了一份导入模块的文件代码。也就是说,执行导入后的某些有副作用的函数,并不会改变原模块的变量值。同时,因为是同步加载模块,服务端的模块都在本地磁盘中,所以很适用于服务端。但是对于浏览器环境,异步方案显然更加适合

2.6.1 特点
  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。
2.6.2 语法
  • 暴露模块:module.exports = valueexports.xxx = value
  • 引入模块:require(xxx),如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径

此处我们有个疑问:CommonJS暴露的模块到底是什么? CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性通过require引入的文件,自动挂载到了当前文件的children属性中。挂载过后,被挂载的文件会随着引入文件执行。同时这个被挂载的文件代码可以理解为引入文件的私有代码,与被引入文件无瓜。此时我们可以通过module.children[0].exports拿到被导入文件的各种方法和变量

javascript 复制代码
// example.js
var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

上面代码通过module.exports输出变量x和函数addX。

javascript 复制代码
var example = require('./example.js');//如果参数字符串以"./"开头,则表示加载的是一个位于相对路径
console.log(example.x); // 5
console.log(example.addX(1)); // 6

require命令用于加载模块文件。require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错

3. 客户端模块化

3.1 客户端模块化和服务端模块化有什么区别?

服务端加载一个模块,直接就从硬盘或者内存中读取了,消耗时间可以忽略不计

浏览器需要从服务端下载这个文件,所以说如果用 CommonJS 的 require 方式加载模块,需要等代码模块下载完毕,并运行之后才能得到所需要的API。

3.2 为什么 CommonJS 不适用于前端模块?

如果我们在某个代码模块里使用CommonJS的方法require了一个模块,而这个模块需要通过http请求从服务器去取,如果网速很慢,而CommonJS又是同步的,所以将阻塞后面代码的执行,从而阻塞浏览器渲染页面,使得页面出现假死状态。

因此后面AMD规范随着RequireJS的推广被提出,异步模块加载,不阻塞后面代码执行的模块引入方式,就是解决了前端模块异步模块加载的问题。

3.3 AMD(Asynchronous Module Definition) & RequireJS

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要来着早。

模块定义

  1. 独立模块的定义------不依赖其它模块的模块定义
javascript 复制代码
//独立模块定义
define({
  method1: function() {}
  method2: function() {}
});  

//或者
define(function(){
  return {
    method1: function() {},
    method2: function() {},
  }
});
  1. 非独立模块------依赖其他模块的模块定义
javascript 复制代码
define(['math', 'graph'], function(math, graph){
  ...
});

模块引用

javascript 复制代码
require(['a', 'b'], function(a, b){
  a.method();
  b.method();
})

3.4 CMD(Common Module Definition) & SeaJS

CMD------通用模块规范,由国内的玉伯提出。SeaJS------CMD的实现,其实也可以说CMD是SeaJS在推广过程中对模块定义的规范化产出。与AMD规范的主要区别在于定义模块和依赖引入的部分。AMD需要在声明模块的时候指定所有的依赖,通过形参传递依赖到模块内容中:

javascript 复制代码
define(['dep1', 'dep2'], function(dep1, dep2){
  return function(){};
})

与AMD模块规范相比,CMD模块更接近于Node对CommonJS规范的定义:

javascript 复制代码
define(factory);

在依赖示例部分,CMD支持动态引入,require、exports和module通过形参传递给模块,在需要依赖模块时,随时调用require( )引入即可,示例如下:

javascript 复制代码
define(function(require, exports, module){
  //依赖模块a
  var a = require('./a');

  //调用模块a的方法
  a.method();
})

也就是说与AMD相比,CMD推崇依赖就近, AMD推崇依赖前置

3.5 UMD(Universal Module Definition) 通用模块规范

如下是codemirror模块lib/codemirror.js模块的定义方式:

javascript 复制代码
(function (global, factory) {
   typeof exports === 'object' && typeof module !== 'undefined' 
       ? module.exports = factory()          // Node , CommonJS
       : typeof define === 'function' && define.amd  
         ? define(factory)                   //AMD CMD
         : (global.CodeMirror = factory());  //模块挂载到全局
}(this, (function () { 
   ...
})

可以看说所谓的兼容模式是将几种常见模块定义方式都兼容处理。

3.6 什么是运行时加载?

我觉得要从两个点上去理解:

  • CommonJS 和AMD模块都只能在运行时确定模块之间的依赖关系
  • require一个模块的时候,模块会先被执行,并返回一个对象,并且这个对象是整体加载的
javascript 复制代码
//CommonJS 模块
let { basename, dirname, parse } = require('path');

//等价于
let _path = require('path');
let basename = _path.basename, dirname = _path.dirname, parse = _path.parse;

上面代码实质是整体加载path模块,即加载了path所有方法,生成一个对象,然后再从这个对象上面读取3个方法。这种加载就称为"运行时加载"。

3.7 ES6 module

如前面所述,CommonJS和AMD都是运行时加载。ES6在语言规格层面上实现了模块功能,是编译时加载,完全可以取代现有的CommonJS和AMD规范,可以成为浏览器和服务器通用的模块解决方案。这里关于ES6模块我们项目里使用非常多,所以详细讲解。

3.7.1 ES6模块使用------export
  1. 导出一个变量
javascript 复制代码
export var name = 'pengpeng';
  1. 导出一个函数
javascript 复制代码
export function foo(x, y){}
  1. 常用导出方式(推荐)
javascript 复制代码
// person.js
const name = 'dingman';
const age = '18';
const addr = '卡尔斯特森林';

export { firstName, lastName, year };
  1. As用法
javascript 复制代码
const s = 1;
export {
  s as t,
  s as m, 
}

可以利用as将模块输出多次。

3.7.2 ES6模块使用**------import**
  1. 一般用法
javascript 复制代码
import { name, age } from './person.js';
  1. As用法
javascript 复制代码
import { name as personName } from './person.js';

import命令具有提升效果,会提升到整个模块的头部,首先执行,如下也不会报错:

javascript 复制代码
getName();

import { getName } from 'person_module';
  1. 整体模块加载 *
javascript 复制代码
//person.js
export name = 'xixi';
export age = 23;

//逐一加载
import { age, name } from './person.js';

//整体加载
import * as person from './person.js';
console.log(person.name);
console.log(person.age);

ES6模块使用------export default

其实export default,在项目里用的非常多,一般一个Vue组件或者React组件我们都是使用export default命令,需要注意的是使用export default命令时,import是不需要加{}的。而不使用export default时,import是必须加{},示例如下:

javascript 复制代码
//person.js
export function getName() {
 ...
}
//my_module
import {getName} from './person.js';

-----------------对比---------------------

//person.js
export default function getName(){
 ...
}
//my_module
import getName from './person.js';

export default其实是导出一个叫做default的变量,所以其后面不能跟变量声明语句。

javascript 复制代码
//错误
export default var a = 1;

值得注意的是我们可以同时使用 export 和 export default

javascript 复制代码
//person.js
export name = 'dingman';
export default function getName(){
  ...
}

//my_module
import getName, { name } from './person.js';

前面一直提到,CommonJS是运行时加载,ES6是编译时加载,那么两个有什么本质的区别呢?

3.8 ES6 module 与 CommonJS模块加载区别

ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。所以说ES6是编译时加载,不同于CommonJS的运行时加载(实际加载的是一整个对象),ES6模块不是对象,而是通过export命令显式指定输出的代码,输入时也采用静态命令的形式:

javascript 复制代码
//ES6模块
import { basename, dirname, parse } from 'path';

//CommonJS模块
let { basename, dirname, parse } = require('path');

以上这种写法与CommonJS的模块加载有什么不同?

  • 当require path模块时,其实 CommonJS会将path模块运行一遍,并返回一个对象,并将这个对象缓存起来,这个对象包含path这个模块的所有API。以后无论多少次加载这个模块都是取这个缓存的值,也就是第一次运行的结果,除非手动清除。
  • ES6会从path模块只加载3个方法,其他不会加载,这就是编译时加载。ES6可以在编译时就完成模块加载,当ES6遇到import时,不会像CommonJS一样去执行模块,而是生成一个动态的只读引用,当真正需要的时候再到模块里去取值,所以ES6模块是动态引用,并且不会缓存值。

因为CommonJS模块输出的是值的拷贝,所以当模块内值变化时,不会影响到输出的值。基于Node做以下尝试:

javascript 复制代码
//person.js
var age = 18;
module.exports ={
  age: age,
  addAge: function () {
    age++;
  }
} 

//my_module
var person = require('./person.js');
console.log(person.age);
person.addAge();
console.log(person.age);

//输出结果
18
18

可以看到内部age的变化并不会影响person.age的值,这是因为person.age的值始终是第一次运行时的结果的拷贝。

再看ES6

javascript 复制代码
//person.js
export let age = 18;
export function addAge(){
  age++;
}

//my_module
import { age, addAge } from './person.js';
console.log(age);
addAge();
console.log(age);

//输出结果
18
19

可以看到值被修改了,进一步证明了 ES6 的模块化只是对模块文件的引用

3.9 总结

  • CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
  • AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
  • CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案

规范化


规范化的最根本目的就是为了保证团队成员的一致性,从而减少沟通成本,提高开发效率。我以前就经历过因为规范不标准,造成产品与开发理解有偏差、开发各写各的代码,导致各种 BUG 不断,最后项目延期的事。所以说为了提高开发效率,减少加班,请一定要统一规范

自动化


前端自动化是指前端代码的自动化构建、打包、测试及部署等流程。

持续集成(Continuous Integration)CI

持续部署(Continuous Deployment)CD

前端自动化通常与CI/CD流程相结合。

自动化的意义

  • 减少人为失误,提高软件质量
  • 效率迭代,便捷部署
  • 快速交付,便于管理

自动化的实施

  • 通过构建工具快速完成构建、打包
  • 采取自动化测试
  • 采取自动化部署

前端监控


性能优化


重构


相关推荐
迷雾漫步者1 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人5 小时前
前端知识补充—CSS
前端·css
GISer_Jing6 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试