微信小程序组件库开发记录

Posted 在厕所喝茶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微信小程序组件库开发记录相关的知识,希望对你有一定的参考价值。

背景

业界已经有很多功能强大,成熟的微信小程序组件库,比如vant,为什么自己还要搞一套微信小程序组件库出来?

  • 市面上的组件库功能虽然很强大,很成熟,但并不能百分百完全满足我们的需求。比如我们现在使用的vant组件库,实际上开发中,我们需要用到瀑布流密码键盘悬浮按钮回到顶部等组件,这些组件都是vant没有的组件。所以我们需要将这些组件封装起来,发布到 npm 或者私服上面,方便下次使用。
  • 技术能力的提升。其实开发这套小程序组件库也算是造轮子吧。只是在轮子上面添加一些属于自己业务特点的东西。一套流程下来,你会发现学到了不少的东西,自身能力也得到了提升。比如代码规范,git 工作流,工程化等,这些东西都是平时实际开发中很难学到的东西,毕竟实际开发中已经有固定的模板或者脚手架等工具,只需要一行命令就可以把所有环境搭建好,你只需要在上面开发即可,根本就不想要理会其他东西。
  • 提升代码阅读能力。在开始这套组件库开发之前,我去阅读了vant组件库的代码,参考了vant组件库的代码架构,然后根据自己的实际开发工作能力,制定了一套自己的组件库架构。每一个组件开发完毕之后,我会去阅读vant的组件代码,看一下别人的设计思想跟我的设计思想有什么不一样,有什么优缺点,然后改进自己的代码。通过这种方式,我对阅读代码能力有了很大的提升,并且学到了不少的设计思想。
  • 热情和情怀,这一点很重要。这套组件库是利用我下班时间还有周末的时间开发出来的。每天下班都会花费 1-2 小时去搞这个组件库。这也算是我对前端的一种热爱吧,否则也不会坚持下来。热情和情怀就是我坚持搞这套组件库的动力。好在这套组件库搞下来之后,收到不少人的喜爱,有人愿意去看,愿意去使用,虽然不多,但也是对我的一种鼓舞。

前言

这篇文章主要是记录技术选型,环境搭建,组件开发常用技巧还有组件单元测试。希望可以帮助到有需要的同学吧

技术选型

在开始前,我阅读过vant的源码,它是用typescript+less+gulp的。并且内部封装了一个vantComponent函数,感觉这对新手来说阅读起来不太友好。但是我对typescript编写小程序并不是很熟悉,所以我还是选择了javascript这个原汁原味的语言。css 预处理方方面,由于平时工作开发中我都是使用scss的,所以我选择了scss来编写 css。所以我的最终技术选型是javascript+scss+gulp

环境搭建

这套组件库都是基于微信原生的语法来写,并没有借助第三方框架,比如mpvueuni-app等这些框架。所以我们甚至可以连环境都不用搭建就可以进行开发了,只需要新建一个文件夹,然后在文件夹下面新建.wxml.wxss.js.json这些文件就可以进行开发了。但是这种开发模式效率太慢了,缺点如下:

  • 编写 css 的时候,开发过微信小程序的同学应该都知道wxss其实跟 web 的css差不多,并不支持嵌套语法,函数,变量,循环等功能。
  • 开发一个新的组件都要重复同样的工作,就是新建文件夹,然后新建对应的文件,初始化每个文件的模板。
  • 微信小程序对包的大小是有要求的。所以我们需要对文件进行压缩,减少包的体积。如果不进行代码压缩,这将会对使用者来说是个极差的体验。
  • 一般来说,我们有一个编写代码的packages目录,还有一个dist或者lib目录的,这是将来发布到 npm 上会使用到的,同时还有一个小程序演示目录。当我们编写完一个组件的时候,需要借助其他工具拷贝到小程序演示目录下,否则你需要自己手动 cv 一下拷贝过去,这将会大大降低我们的开发效率。

针对上面的缺点,我们需要搭建一套开发环境出来解决上面的问题。

安装 gulp

npm i gulp -D

scss编译为wxss

  • 安装依赖
npm i gulp-sass gulp-clean-css gulp-rename gulp-insert node-sass sass -D
  • 编译
const { src, dest } = require("gulp");
const sass = require("gulp-sass");
const cssmin = require("gulp-clean-css");
const jsmin = require("gulp-uglify-es").default;
const rename = require("gulp-rename");
const insert = require("gulp-insert");
const path = require("path");

const buildWxss = (srcPath, distPath) => () =>
  src(srcPath)
    //  编译scss
    .pipe(sass().on("error", sass.logError))
    // 压缩
    .pipe(cssmin())
    .pipe(
      // 插入内容
      insert.transform((contents, file) => {
        const commonScssPath = `packages${path.sep}common`;
        if (!file.path.includes(commonScssPath)) {
          const relativePath = "../common/base.wxss";
          contents = `@import '${relativePath}';${contents}`;
        }
        return contents;
      })
    )
    .pipe(
      // 将.scss后缀名改成.wxss
      rename((srcPath) => {
        srcPath.extname = ".wxss";
      })
    )
    // 输出到指定目录
    .pipe(dest(distPath));

压缩wxmljsjson文件和图片

  • 安装依赖
npm i gulp-htmlmin gulp-uglify-es gulp-jsonminify gulp-imagemin -D
  • 压缩
const { src, dest } = require("gulp");
const wxmlmin = require("gulp-htmlmin");
const jsmin = require("gulp-uglify-es").default;
const jsonmin = require("gulp-jsonminify");
const imagemin = require("gulp-imagemin");

// 压缩wxml
const buildWxml = (srcPath, distPath) => () =>
  src(srcPath)
    .pipe(
      wxmlmin({
        removeComments: true,
        keepClosingSlash: true,
        caseSensitive: true,
        collapseWhitespace: true,
      })
    )
    .pipe(dest(distPath));
// 压缩js
const buildJs = (srcPath, distPath) => () =>
  src(srcPath).pipe(jsmin()).pipe(dest(distPath));
// 压缩json
const buildJson = (srcPath, distPath) => () =>
  src(srcPath).pipe(jsonmin()).pipe(dest(distPath));
// 压缩图片
const buildImage = (srcPath, distPath) => () =>
  src(srcPath).pipe(imagemin()).pipe(dest(distPath));

拷贝文件到另一个目录

const { src, dest, parallel } = require("gulp");
const copy = (srcPath, distPath, ext) => () => {
  return src(`${srcPath}/*.${ext}`).pipe(dest(distPath));
};
const copyStatic = (srcPath, distPath) => {
  return parallel(
    copy(srcPath, distPath, "wxml"),
    copy(srcPath, distPath, "wxs"),
    copy(srcPath, distPath, "json"),
    copy(srcPath, distPath, "js"),
    copy(srcPath, distPath, "png")
  );
};

删除目录

  • 安装依赖
npm i del -D
  • 删除
const del = require("del");
const clean = (cleanPath) => () =>
  del(cleanPath, {
    force: true,
  });

整合

const { series, parallel, watch } = require("gulp");

const path = require("path");

const distPath = path.resolve(__dirname, "../dist");

const examplePath = path.resolve(__dirname, "../examples/dist");

let packagesPath = path.resolve(__dirname, "../packages");

packagesPath = `${packagesPath}/**`;

module.exports = {
  // 打包
  build: series(
    clean(distPath),
    parallel(
      buildWxss(`${packagesPath}/*.scss`, distPath),
      buildWxml(`${packagesPath}/*.wxml`, distPath),
      buildImage(`${packagesPath}/*.png`, distPath),
      buildJson(`${packagesPath}/*.json`, distPath),
      buildJs(`${packagesPath}/*.js`, distPath),
      buildWxs(`${packagesPath}/*.wxs`, distPath)
    )
  ),
  //   开发环境,拷贝packages目录下面的组件到小程序演示目录下
  dev: series(
    clean(examplePath),
    parallel(
      buildWxss(`${packagesPath}/*.scss`, examplePath),
      copyStatic(packagesPath, examplePath)
    )
  ),
  //   监听packages目录文件变化,拷贝变化文件到小程序演示目录下
  watch: parallel(() => {
    watch(
      "../packages/**/*.scss",
      buildWxss(`${packagesPath}/*.scss`, examplePath)
    );
    watch("../packages/**/*.wxml", copy(packagesPath, examplePath, "wxml"));
    watch("../packages/**/*.wxs", copy(packagesPath, examplePath, "wxs"));
    watch("../packages/**/*.json", copy(packagesPath, examplePath, "json"));
    watch("../packages/**/*.js", copy(packagesPath, examplePath, "js"));
    watch("../packages/**/*.png", copy(packagesPath, examplePath, "png"));
  }),
};

package.json新增如下 script 脚本命令行

  "scripts": {
    "dev": "gulp -f build/index.js dev",
    "build": "gulp -f build/index.js build",
    "watch": "gulp -f build/index.js watch",
  }

创建组件模板

我们使用 node 命令行来代替手动创建组件文件和模板

const fs = require("fs");
const path = require("path");
// 模板
const template = require("./template.js");
const argv = process.argv;
// 获取创建的组件名
const componentName = argv[2];
// 将组件名转化为-连接
const componentNameLine = componentName
  .replace(/([A-Z])/g, "-$1")
  .toLowerCase()
  .substring(1);
// 组件开发目录
const packagesPath = path.resolve(__dirname, "../packages");
// 组件js模板
const compJsTemplate = template.compJsTemplate();
// 组件json模板
const compJsonTemplate = template.compJsonTemplate();
// 组件scss模板
const compScssTemplate = template.compScssTemplate(componentNameLine);
// 组件wxml模板
const compWxmlTemplate = template.compWxmlTemplate(componentNameLine);
// 创建文件夹
function createDir(pathSrc) {
  try {
    fs.statSync(pathSrc);
    console.log(`${componentName} 文件夹已经存在`);
    return false;
  } catch (error) {
    fs.mkdirSync(pathSrc);
    return true;
  }
}
// 创建组件模板
function createPackagesFile(pathSrc) {
  try {
    const wxml = path.resolve(pathSrc, "./index.wxml");
    const json = path.resolve(pathSrc, "./index.json");
    const js = path.resolve(pathSrc, "./index.js");
    const scss = path.resolve(pathSrc, "./index.scss");
    fs.writeFileSync(wxml, compWxmlTemplate);
    fs.writeFileSync(json, compJsonTemplate);
    fs.writeFileSync(js, compJsTemplate);
    fs.writeFileSync(scss, compScssTemplate);
    return true;
  } catch (error) {
    console.log("创建文件失败");
    return false;
  }
}

function createPackageComponent() {
  const pathSrc = path.resolve(packagesPath, componentName);
  const result = utils.createDir(pathSrc);
  if (result) {
    const flag = utils.createPackagesFile(pathSrc);
  }
}

createPackageComponent();

package.json新增如下 script 脚本命令行

  "scripts": {
    "add": "node ./build/createComponent.js"
  },

使用

npm run add button

开发技巧

微信小程序给我们提供了很多的组件,api 等强大的功能,但是实际上在组件库开发的过程中,来来去去都是那几样东西。掌握下面组件开发常用的技能,基本上能够开发出 99%的组件了。

properties 父组件给子组件传递数据

properties是用来父子组件用来进行通讯的。这个跟vue的 props 十分相似。主要有下面四个参数:

  • type:定义数据的类型,只能是单个类型
  • optionalTypes:定义数据的类型,当数据有可能是Boolean或者Number等多种类型时,使用该字段
  • value:初始值。切记是value,可能受vue的影响,我经常会写成default
  • observer:值变化回调,可以是一个函数或者字符串。如果是字符串,就会调用methods下面的同名函数。但是现在不推荐使用这个字段了,而是推荐使用Component构造器的observers,这个功能和性能会更好

代码示例:

Component({
  properties: {
    // 简写
    disabled: Boolean,
    block: {
      type: Boolean,
      value: false,
    },
    iconSize: {
      optionalTypes: [String, Number],
    },
    plain: {
      type: Boolean,
      value: false,
      observer: "setColor",
    },
  },
  methods: {
    setColor() {},
  },
});

behaviors 公共行为

behaviors实际上是用来定义行为的。通常来说,如果多个组件存在相同的行为,那么就可以将这些公共行为提取出来,封装成behaviors,这样多个组件就可以共享了。behaviors的写法跟Component构造器的写法实际上是一样的。熟悉vue开发的同学应该知道这其实就是跟mixins的功能一样的。而且微信小程序中内置了三个form表单组件的behaviors,这三个behaviors都是用来开发表单组件的。详情可查看这里

代码示例:

// behaviors/button.js
const ButtonBehavior = Behavior({
  properties: {
    // 标识符
    id: String,
    // 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文
    lang: String,
    // 客服消息子商户
    businessId: Number,
    // 会话来源
    sessionFrom: String,
    // 会话内消息卡片标题
    sendMessageTitle: String,
    // 会话内消息卡片点击跳转小程序路径
    sendMessagePath: String,
    // 当前分享路径
    sendMessageImg: String,
    // 显示会话内消息卡片
    showMessageCard: Boolean,
    // 打开 APP 时,向 APP 传递的参数
    appParameter: String,
    // 无障碍访问
    ariaLabel: String,
  },
});

export default ButtonBehavior;

// Button/index.js
import ButtonBehavior from "behaviors/button";

Component({
  behaviors: [ButtonBehavior, "wx://form-field-button"],
});

options

options主要用到的有 2 个参数,分别如下:

  • addGlobalClass:
    • true:页面的样式会影响到自定义组件组件内部的样式,但是自定义组件样式不能影响到页面的样式。好处就是页面可以很方便的改写自定义组件中的样式。但是自定义组件中的样式并不能影响到在该组件内部使用的自定义样式,需要借助externalClasses字段,下面会讲到的
    • false:开启样式隔离,组件和页面样式互不影响
    • 关于样式隔离,详情可以看这里
  • multipleSlots:当wxml中需要用到多个slot插槽的时候,必须将这个属性置位true,不然插槽不生效。只有一个slot插槽的时候可不设置。

代码示例:

Component({
  options: {
    addGlobalClass: true,
    multipleSlots: true,
  },
});

externalClasses 外部样式类

外部样式类,可指定那些类受页面影响,但是由于外部样式类和普通样式类的优先级没有定义,所以一般来说都需要在外部样式类中添加!important。这个字段在组件开启了样式隔离或者在自定义组件中使用自定义组件,提供给外部修改组件内部样式的一种方法。详情可查看这里

代码示例:

Button 组件

以上是关于微信小程序组件库开发记录的主要内容,如果未能解决你的问题,请参考以下文章

微信小程序代码片段

全栈之路-微信小程序-SKU开发(代码)

微信小程序自定义组件详解

微信小程序UI组件库推荐

微信小程序 自定义组件入门

iView Weapp微信小程序UI组件库中i-input的使用