一步步解读Swiper源码

·  阅读 1627

image.png

一、介绍

Swiper是纯javascript打造的滑动特效插件,面向手机、平板电脑等移动终端。Swiper能实现触屏焦点图、触屏Tab切换、触屏轮播图切换等常用效果。

目前,Swiper在业界得到了广泛的应用,去年因为公司基础数据部门需要针对Swiper增加图片旋转的功能,借此机会对Swiper源码进行了阅读,并扩展了rotate组件。

本文,将从如何阅读swiper@5.3.6源码,到定制开发入手,给大家介绍一下我的思路,希望可以给大家一些启发和思考。

二、如何一步步分析源码

通常阅读源码,我会按照以下几个步骤入手:

  1. Readme.md
  2. package.json
  3. 打包工具
  4. 入口文件
  5. 最小开发单元

2.1 Readme.md

Readme通常是项目的一个概要介绍,会告诉我们项目如何安装,编译打包,如何使用,以及常用的API,简单的代码示例,通过阅读我们可以对项目有一个大致的理解,让我们后面在深入细节的时候,不会迷失方向。

下面是Swiper项目Readme.md中的部分内容:

安装npm包

$ npm install --global gulp # 全局安装gulp工具
$ npm install # 安装本项目的依赖
复制代码

打包开发版本

$ npm run build:dev 
复制代码

打包结果在build/ 文件夹。

打包生产版本

$ npm run build:prod
复制代码

打包结果在 package/ 文件夹。

😭😭😭,不幸的是,并没有关于如何在本地启动开发环境的介绍,那么接下来,让我们分析一下package.json。。。

2.2 package.json

package.json中,首先需要关注scriptsdependenciesdevDependencies

通过scripts我们可以推断出本地开发的命令,因为包含gulp server,打包的命令;

# [gulp build 打包];[gulp playground 演示环境准备];[gulp server 本地服务];
"dev": "cross-env NODE_ENV=development gulp build && cross-env NODE_ENV=development gulp playground && cross-env NODE_ENV=development gulp server",
# 本地开发脚本
npm run dev | yarn dev
复制代码

当然还有一些其他的命令,比如lint是执行eslint,test执行lint后进行dev打包,等等。

通过dependencies我们可以知道项目的依赖,只有两个库,说明swiper是一个比较纯净的项目,大部分功能都是独立完成的。

"dependencies": {
    "dom7": "^2.1.3", # dom操作封装
    "ssr-window": "^1.0.1"
}
复制代码

通过devDependencies我们可以知道工程化工具、编译工具等,下面的配置删减了一部分,列举出的可以帮助大家分析出这个项目打包的工具。

"devDependencies": {
    "eslint": "^6.4.0", // 代码风格校验
    "gulp": "^4.0.2", // 打包编译工具
    "less": "^3.10.3", // less编译
    "postcss": "^7.0.18", // css处理
    "rollup": "^1.21.4", // JavaScript模块打包器
},
复制代码

2.3 打包工具

通过分析package.json我们发现了gulprollupless,很容易联想到,项目是使用gulp来作为工程化工具,使用rollup来打包 js 代码,使用less来编译less文件;

接下来让我们抽丝剥茧,分析gulpfile.js,打开一看,哎呀,只有一行代码

require('./scripts/gulpfile');
复制代码

所有和打包相关的代码,都被安排到了scripts目录中。

gulp的理念和Grunt很像,就是task,也就是任务,然后通过一连串的任务顺序执行,就完成了代码的编译打包,然后输出结果。

下面是对本项目中,gulp 任务的分析:其中核心的serverbuild两个任务。

  1. server任务的目标是在本地启动一个web服务connect,然后通过监控本地代码watch,有代码改动,就会去重新打包js和css,最后打开playground对应的在线测试地址open
  2. build任务的目标就是打包项目代码,要走的任务也就是jscss打包。
graph TB
A[tasks] --> B[playgroud]
A[tasks] --> C[server] 
A[tasks] --> D[build]
C[server] --> G[watch]
C[server] --> H[connect]
C[server] --> I[open]
D[build] --> K[styles]
D[build] --> J[js]
J[js] --> L[es]
J[js] --> M[umd]
K[styles] --> N[less]
G[watch] --> O[js]
G[watch] --> P[styles]

2.3.1 task 之 js

scripts/build-js.js

我们发现支持了 esumd两个打包的方法,其中esm是ES6提出的标准模块系统,umd全称是通用模块定义规范(Universal Module Definition),是集结了commonjsamdcmd于一身的方案,即写一套代码,可运行于浏览器、服务端等不同场景。

使用rollup进行打包,这里需要关注一下rollup->plugins->replace,比如代码中存在'//INSTALL_COMPONENTS'的地方会被插入后面的代码字符串,这里就是单纯的字符串替换。

replace({
    delimiters: ['', ''],
    'process.env.NODE_ENV': JSON.stringify(env),
    '//IMPORT_COMPONENTS': components.map((component) => `import ${component.capitalized} from './components/${component.name}/${component.name}';`).join('\n'),
    '//INSTALL_COMPONENTS': components.map((component) => `${component.capitalized}`).join(',\n  '),
    '//EXPORT': 'export default Swiper',
}),
复制代码

搜一下代码可以发现,以下代码中存在'//INSTALL_COMPONENTS',所以,会有一批组件名称会被插入到这个位置,//IMPORT_COMPONENTS等也是类似的。

// 路径:src/swiper.js
const components = [
  Device,
  Support,
  Browser,
  Resize,
  Observer,
  //INSTALL_COMPONENTS
];
复制代码

scripts/build-config.js

打包配置文件,记录了一些常量,比如components记录了swiper中所有组件,新增组件时,就需要将名字加到这里。

2.3.2 task 之 styles

scripts/build-styles.js

主要是使用less,对less文件进行编译打包,这里不多讲了。

2.4 入口文件

先来看一下,我们平时是怎么使用Swiper的,通常情况下是有两个参数:

var swiper = new Swiper(container,options);

  • 一个是container,是选择器(字符串或者是HTML Element对象);
  • 一个是配置项options,是个对象:
// 举例
var swiper = new Swiper('.swiper-container', {
  pagination: {
    el: '.swiper-pagination',
    dynamicBullets: true,
  },
});
复制代码

所以说,我们通过new Swiper(),初始化了一系列组件(比如:pagination),所以我们理解源码也要从Swiper类开始。

2.4.1 从swiper.js开始

好,我们来分析Swiper的源码,在scripts/build-js.js中已经提示我们了,入口文件如下:

input: './src/swiper.js',
复制代码

src/swiper.js

找到swiper.js,第一行代码如下,鸡贼的你也许已经发现,这会不会就是Swiper的类定义:

// Swiper Class
import Swiper from './components/core/core-class';
复制代码

src/components/core/core-class.js

import SwiperClass from '../../utils/class';
class Swiper extends SwiperClass {
// ...
}
复制代码

顺藤摸瓜,我们找到core-class,发现它居然是继承自SwiperClass,也就是utils/class,于是我们顺路去看看。

src/utils/class.js

哎呀,SwiperClass终于到目的地了,以下是这两个类的类图,列举了主要的方法和属性。

图2:Swiper类图

接下来,结合下面的泳道图,我们将会重点分析这三个js。

图3:Swiper函数调用

2.4.2 加载swiper.js后,构造Swiper类

让我们来看一下swiper.js

// Swiper Class
import Swiper from './components/core/core-class';

//IMPORT_COMPONENTS
const components = [
  Device,
  Support,
  Browser,
  Resize,
  Observer,
  //INSTALL_COMPONENTS
];
 
if (typeof Swiper.use === 'undefined') {
  Swiper.use = Swiper.Class.use;
  Swiper.installModule = Swiper.Class.installModule;
}

// 执行了use方法,对组件进行了处理,让我们看看 use 方法在干嘛
Swiper.use(components);
复制代码

根据上面类图,我们可以看到use方法在utils/class,是一个静态方法,逐行代码来读读看:

/**
 * 类似于Vue的use方法
 *
 * @static
 * @param {*} module 组件或者模块
 * @param {*} params 
 * @returns
 * @memberof SwiperClass
 */
static use(module, ...params) {
  const Class = this; // Class就是SwiperClass
  if (Array.isArray(module)) { // 如果传入数组就遍历注册
    module.forEach((m) => Class.installModule(m));
    return Class;
  }
  // 如果单个组件,就直接注册,是不是也提供了运行中注册组件的机会?
  return Class.installModule(module, ...params);
}

/**
 * 注册单个组件
 *
 * @static
 * @param {*} module
 * @param {*} params
 * @returns
 * @memberof SwiperClass
 */
static installModule(module, ...params) {
  const Class = this;
  if (!Class.prototype.modules) Class.prototype.modules = {};
  
  // 模块名字,默认使用name,没有则随机生成一个
  const name = module.name || (`${Object.keys(Class.prototype.modules).length}_${Utils.now()}`);
  // 将组件加到SwiperClass的modules对象中
  Class.prototype.modules[name] = module;
  
  // 将module.proto上的方法绑定到SwiperClass.prototype
  if (module.proto) {
    Object.keys(module.proto).forEach((key) => {
      Class.prototype[key] = module.proto[key];
    });
  }
  
  // Class
  // 将module.static上的方法绑定到SwiperClass
  if (module.static) {
    Object.keys(module.static).forEach((key) => {
      Class[key] = module.static[key];
    });
  }
  
  // 调用install方法
  if (module.install) {
    module.install.apply(Class, params);
  }
  return Class;
}
复制代码

So,经过useinstallModule方法之后,SwiperClass.prototype.modules对象上包含了所有的组件对象,SwiperClass.prototypeSwiperClass绑定了一些方法和属性。

2.4.3 new Swiper() 实例

经过use方法,Swiper已经准备好了,可以开始初始化Swiper实例了。

var swiper = new Swiper('.swiper-container', {
  pagination: {
    el: '.swiper-pagination',
    dynamicBullets: true,
  },
});
复制代码

这里可以看上面的流程图,就是将Swiper的constructor方法执行一遍,会去合并入参,会执行每个组件的create方法,每个组件on下的事件都汇总到eventsListeners中,然后通过swiper.emit('init');,触发所有组件的初始化。

重要的是执行了一下两个方法


// Install Modules
swiper.useModules();

// Init
if (swiper.params.init) {
  swiper.init();
}
复制代码
useModules
useModules(modulesParams = {}) {
  // swiper实例
  const instance = this;
  if (!instance.modules) return;
  // 遍历 modules
  Object.keys(instance.modules).forEach((moduleName) => {
    const module = instance.modules[moduleName];
    const moduleParams = modulesParams[moduleName] || {};

    // Extend instance methods and props
    // 组件如果有instance对象,将instance的属性遍历绑定到instance上,同时绑定this对象指向。这里module.instance稍微和instance有点歧义。
    if (module.instance) {
      Object.keys(module.instance).forEach((modulePropName) => {
        const moduleProp = module.instance[modulePropName];
        if (typeof moduleProp === 'function') {
          instance[modulePropName] = moduleProp.bind(instance);
        } else {
          instance[modulePropName] = moduleProp;
        }
      });
    }
    
    // Add event listeners
    // 将组件on属性上的事件和处理方法遍历,调用swiper实例的on方法,分类绑定到eventsListeners
    if (module.on && instance.on) {
      Object.keys(module.on).forEach((moduleEventName) => {
        instance.on(moduleEventName, module.on[moduleEventName]);
      });
    }

    // Module create callback
    // 如果组件存在create方法,执行create方法,而且对象是swiper实例。
    if (module.create) {
      module.create.bind(instance)(moduleParams);
    }
  });
}

复制代码
init
init() {
  const swiper = this;
  if (swiper.initialized) return;

  // 触发beforeInit事件
  swiper.emit('beforeInit');

  // ... 省略代码 

  // 根据浏览器环境,配置touch、mouse等事件。
  swiper.attachEvents();

  // Init Flag
  swiper.initialized = true;

  // 通知eventsListeners中所有的init事件执行
  swiper.emit('init');
}
复制代码

到此为止,我们已经分析了Swiper的组件注册,类和实例初始化的过程了,后面我们可以基于一个组件进行分析,了解如何设计、开发一个组件。

2.4.4 小结

  1. 当我们通过CDN加载swiper.js后,会先执行代码构造Swiper类,主要过程是将组件注册到Swiper类的prototype对象上,核心方法是useinstallModule
  2. new Swiper后,会执行Swiper类的constructor方法,开始基于指定的container去实例化轮播器,合并参数,注册事件,然后初始化组件,将轮播器运转起来,核心方法是useModulesuseModulesParamsinit等。

2.5 分析一个组件

我们拿pagination举例,如下图,Pagination是封装了一些常用方法,export出去的对象包含四个属性,nameparamscreateoncreateonnew Swiper的时候会被调用,nameparams名称和初始化参数。

2.5.1 Pagination对象

定义了一些分页器的常用方法,相当于抽取公共方法,在create方法中被绑定到了pagination对象上,后续会被调用,包含注册监听事件,以及对应的处理方法,组件的核心功能都是在这些方法中实现的。

2.5.2 params

定义了分页器的默认参数,在useModulesParams方法中会合并到swiper实例对象的params属性中;

2.5.3 create

create方法,在useModules的时候会被调用,初始化Swiper实例的pagination属性;

2.5.4 on

on属性下的所有方法,在useModules的时候,会被分类保存到eventsListeners对象中,在emit具体的事件的时候触发,这提供了一种全局通知的方式,比如前面提到的init事件,就会在swiper对象实例化的时候通知,然后去初始化Swiper的各个组件;

目前已知的事件,文档介绍,当你要自定义一个新组件的时候,也应该会用到其中的一些事件。

init,touchStart,touchMove,touchEnd,slideChangeTransitionStart,
slideChangeTransitionEnd,imagesReady,transitionStart,transitionEnd,
touchMoveOpposite,sliderMove,click,tap,doubleTap,progress,reachBeginning,
beforeDestroy,reachEnd,setTransition,resize,setTranslate,
slideNextTransitionStart,slideNextTransitionEnd,slidePrevTransitionStart,
slidePrevTransitionEnd,fromEdge,toEdge,slideChange,autoplayStart,
autoplayStop,autoplay,beforeLoopFix,loopFix,observerUpdate,breakpoint
复制代码

三、开发一个新组件

参考pagination,我们希望增加一个旋转按钮,在点击的时候,当前图片可以旋转,代码结构如下,结构基本和pagination一致。

image.png

四、ending

至此,简单的源码分析就算完成了,对swiper的源码结构如何组织,代码执行的逻辑有了一定的认识,对于如何扩展一个组件也有了方案,当然还有很多细节性的东西,后面大家可以再去详细的阅读一下。


南京三百云信息科技有限公司(车300)成立于2014年3月27日,是一家扎根于南京的移动互联网企业,目前坐落于南京、北京。经过7年积累,累计估值次数已达52亿次,获得了国内外多家优质投资机构青睐如红杉资本、上汽产业基金等。
三百云是国内优秀的以人工智能为依托、以汽车交易定价和汽车金融风控的标准化为核心产品的独立第三方的汽车交易与金融SaaS服务提供商。

欢迎加入三百云,一起见证汽车行业蓬勃发展,期待与您携手同行!
官网:www.sanbaiyun.com/
投递简历:hr@che300.com,请注明来自掘金😁

分类:
前端
收藏成功!
已添加到「」, 点击更改