前端造轮子(组件封装,脚手架搭建)

203 阅读4分钟

为什么要重复造轮子?

明明市场上已经有了很多优秀的开源组件库,公共类库,脚手架,为什么很多公司还是要花很多人力物力打造公司内部的一些公共库呢?

  • 业务量大,各个团队之间业务隔离,重复写业务组件,标准不统一,一旦业务发生变动,各个团队都要自己去改代码
  • 保证公司产品样式风格统一
  • 保证产品的稳定性(开源库版本的不稳定,可能导致生产出问题)
  • 公司团队打造的公共类库相对开源库来说,对解决内部的业务痛点更有针对性
  • 升级方便,公共团队可以随时随地发版修复生产bug,业务团队只需要根据公共团队的通知进行版本升级即可

基于以上原因,很多公司会成立公共团队来打造公共类库,对公司来说,内部的技术可以有效沉淀和积累;对技术团队来说,也可以持续保持对前沿技术的学习和关注。

组件库封装分析

我们以市场上很热门的element-ui 为例,该组件库主要有以下几种类型组件

  • Basic组件:layout、图标、按钮等组件
  • Form组件:输入框、单选框、多选框、日期等结合校验开源库async-validator进行封装。仔细观察,其实该类组件都基于input标签type封装而来
  • Data组件:主要以table为主,比较复杂,一般结合开源vxe-table做二次封装
  • Notice组件:结合vue的一些API进行封装,例如vue.extend;该类组件比较考验工程师动画、样式、js原型等综合实战能力
  • Navigation组件:菜单、标签页等;需要对插槽使用有比较透彻对了解
  • Others组件:其他符合公司业务的组件

我们深入观察下组件库的一些设计,会发现有一些差异,比如,表单类的组件都是使用v-model进行组件传参,而其他组件基本都是使用props进行传参,这是为什么呢?

<test v-model=”value”  />

<test :value=”value” />

思考一下:这两种组件封装有什么异同点?什么时候该用v-model?什么使用该用props呢?

  • v-model:双向数据更新,vue实现mvvm双向绑定的语法糖,数据更新,视图跟着更新,用户输入操作导致的视图更新,也会使代码里的变量更新,实际上通过封装指令,省略了在父组件接收子组件emit事件更新视图的这一步骤
  • v-bind:value:单向数据更新,仅仅代表着父组件传递到子组件,如果要实现双向数据更新,则必须通过sync修饰符(或者通过在父组件接收子组件派发的emit事件),否则达不到双向更新的效果

两者都是数据绑定的语法糖,基于vue指令进行封装,至于相关指令的实现原理,感兴趣的可以自行查阅源码了解,这里就不展开讲了。

以上,我们可以看出,v-model适合封装由用户进行数据视图双向更新的组件,而v-bind:value则更适合封装通过代码赋值变量更新视图的组件

v-model的运用

// 封装输入框
<template>
  <input type="text" :value="value" @input="handleInput" />
</template>

<script>
export default {
  name: 'MyInput',
  props: {
    value: {
      type: String,
      default: '',
    },
  },
  methods: {
    handleInput(e) {
      this.$emit('input', e.EventTarget.value)
    },
  },
}
</script>

<style></style>
// 可以通过model指定对应的props和输出事件
<template>
  <input type="text" :value="text" @input="handleInput" />
</template>

<script>
export default {
  name: 'MyInput',
  model: {
    prop: 'text',
    event: 'input-event',
  },
  props: {
    text: {
      type: String,
      default: '',
    },
  },
  methods: {
    handleInput(e) {
      this.$emit('input-event', e.EventTarget.value)
    },
  },
}
</script>

<style></style>

// 依次类推,可以根据input的属性封装单选框、多选框、上传等组件
<template>
  <input type="checked" :value="text" @input="handleInput" />
</template>

<script>
export default {
  name: 'Checked',
  model: {
    prop: 'checked',
    event: 'change',
  },
  props: {
    checked: {
      type: Boolean,
      default: false,
    },
  },
  methods: {
    handleInput(e) {
      this.$emit('change', e.EventTarget.value)
    },
  },
}
</script>

<style></style>

脚手架搭建

同样,前端存在很多优秀脚手架,如vue-cli、create-react-app等,为什么我们还需要自己搭建一套脚手架呢?

虽然有的脚手架已经实现了插拔式构建,但是生成的模版可能还是不会满足我们的项目需求,而且各团队之间的框架也可能会出现库版本和设计的不一致,比如前端项目vue、axios、router库版本锁定,格式化配置,文件目录架构,公共全局样式,打包配置,多环境开发等,我们如果每个团队开发一个项目都要重新搭建一套,这样就很浪费时间。所以根据需求去定制一套自己的脚手架,做到随时下载,开箱即用的效果,既节省时间,效率也会提升。

创建目录lyson-cli

image.png

package.json文件

// 安装以下依赖
{
  "name": "lyson-cli",
  "version": "1.0.0",
  "description": "自定义脚手架",
  "main": "./bin/index.js",
  "bin": {
    "lyson-cli": "./bin/index.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "lyson",
  "license": "ISC",
  "dependencies": {
    "chalk": "^3.0.0",
    "co": "^4.6.0",
    "commander": "^4.1.1",
    "download": "^8.0.0",
    "fs-extra": "^9.1.0",
    "handlebars": "^4.7.6",
    "inquirer": "^7.1.3",
    "log-symbols": "^3.0.0",
    "ora": "^5.1.2",
    "thunkify": "^2.1.2"
  },
  "engines": {
    "node": ">=8.6"
  }
}

bin/index.js

#!/usr/bin/env node

const program = require("commander");

const initProject = require("../bin/create.js");

// 版本输出
program.version(require("../package.json").version, "-v, --version");

// 项目创建
program
  .name("lyson-cli")
  .usage("<command> [options]")
  .command("create <projectName>")
  .description("创建项目")
  .action(async (projectName) => {
    initProject(projectName);
  });

program.parse(process.argv);

bin/create.js

#!/usr/bin/env node

// 用于文件操作
const fse = require("fs-extra");
// 请求ora库,用于初始化项目等待动画
const ora = require("ora");
// 请求chalk库
const chalk = require("chalk");
// 请求log-symbols库
const symbols = require("log-symbols");
// 请求inquirer库,用于控制交互
const inquirer = require("inquirer");
// 请求handlebars库,用于替换模版字符串
const handlebars = require("handlebars");
// 路径
const path = require("path");

async function initProject(projectName) {
  try {
    const exists = await fse.pathExists(projectName);
    if (exists) {
      console.log(symbols.error, chalk.red("创建失败,项目名重复!"));
    } else {
      inquirer
        .prompt({
          name: "tag",
          type: "list",
          choices: ["vue2", "vue3", "react"],
          message: "请选择项目模版",
        })
        .then(async (option) => {
          const initSpinner = ora(chalk.cyan("正在初始化项目..."));
          initSpinner.start(); // 开始
          
          // 获取模版路径下的文件
          const templatePath = path.resolve(
            __dirname,
            `../template/${option.tag}`
          );
          // 获取当前路径
          const processPath = process.pwd();
          // 项目名小写
          const lowerProjectName = projectName.toLowerCase();
          // 目标路径
          const targetPath = `${processPath}/${lowerProjectName}`;

          try {
            // 拷贝模版到目标路径
            await fse.copy(templatePath, targetPath);
          } catch (error) {
            console.log(symbols.error, chalk.red(`拷贝模版失败.${error}`));
            process.exit();
          }

          try {
            // 读取package.json文件
            const multFilesContent = fse.readFile(
              `${targetPath}/package.json`,
              "utf-8"
            );
            // 读取结果
            const multFilesResult = await handlebars.compile(multFilesContent)({
              project_name: lowerProjectName,
            });
            // 替换已拷贝项目package.json文件里的项目名
            await fse.outputFile(`${targetPath}/package.json`, multFilesResult);
          } catch (error) {
            console.log(
              symbols.error,
              chalk.red(`package.json项目名替换失败.${error}`)
            );
            initSpinner.fail();
            process.exit();
          }

          initSpinner.succeed("项目初始化成功");

          console.log(`
            To get started:
            cd ${chalk.yellow(lowerProjectName)}
            ${chalk.yellow("npm install")} or ${chalk.yellow("yarn install")}
            ${chalk.yellow("npm run dev")} or ${chalk.yellow("yarn run dev")}
          `);
        });
    }
  } catch (e) {
    console.log(symbols.error, chalk.red(`项目创建失败.${error}`));
  }
}

module.exports = initProject;

template目录存放各技术栈的初始化配置文件

本地调试

bin/test

!/usr/bin/env node
console.log('cli测试')

我们bin目录下创建test.js文件,并把test文件作为入口,使用npm-link命令软链接到全局,代码第一行#!/usr/bin/env node,这是告诉系统使用环境中的node执行此文件,打开终端,输入lyson-cli,就会发现成功打印“cli测试”,调试成功后再把入口文件改回index即可.

如何上传到npm

  1. 注册npm账户,这里根据npm官方提示走就可以

image.png

2.本地登录npm(输入你注册的账户,后面npm还会给你发送验证码需要验证邮箱,输入你绑定邮箱即可)

image.png

3.上传npm,检查你package.json文件信息如果没有问题执行npm publish 进行上传