发布vue3组件到npm

目录

准备你的组件

创建项目

创建步骤

创建组件

封装我们要得组件

导出组件

打包

使用组件

发布包

创建npm账户

登录npm

切换npm源

发布到npm

package.json

package.json字段

确定版本号

版本号格式

版本号递增逻辑

[vite 打包库模式](#vite 打包库模式)

库模式配置

build配置项

build.lib

build.rollupOptions


当你开发Vue 3应用程序时,有时你可能会需要将自己创建的组件发布到npm上,以便其他开发者可以轻松地在他们的项目中使用这些组件。本文将指导你如何将Vue 3组件发布到npm。

准备你的组件

首先,确保你已经创建了你的Vue 3组件,并且它在本地正常工作。你可以使用Vue CLI或手动创建组件,然后使用Vue 3的组件系统进行开发。

创建项目

npm init vite@latest

创建步骤

提示我们要安装create-vite@4.1.0得依赖,选择y

起一个组件名字,然后我们选择vue

这里我选择的是javascript,然后回车

安装完成

因为我们需要了element-ui组件库,所以我们要手动安装一下依赖

npm install element-plus --save

创建组件

首先,我们要在src\components目录下,创建一个femessage文件夹,

在在femessage文件夹下创建el-form-renderer.vue 文件,(自定义名字)

封装我们要得组件

<template>
  <div>
    <el-form ref="myelForm" v-bind="$attrs" :model="value" class="el-form-renderer">
      <template v-for="item in innerContent" :key="item.id">
        <slot :name="`id:${item.id}`" />
        <slot :name="`$id:${item.id}`" />

        <component
          :is="item.type === GROUP ? RenderFormGroup : RenderFormItem"
          :ref="
            (el) => {
              customComponent[item.id] = el;
            }
          "
          :data="item"
          :value="value"
          :item-value="value[item.id]"
          :disabled="
            disabled ||
            (typeof item.disabled === 'function' ? item.disabled(value) : item.disabled)
          "
          :readonly="readonly || item.readonly"
          :options="options[item.id]"
          @updateValue="updateValue"
        />
      </template>
      <slot />
    </el-form>
  </div>
</template>

<script setup>
import RenderFormGroup from "./components/render-form-group.vue";
import RenderFormItem from "./components/render-form-item.vue";
import {
  reactive,
  computed,
  ref,
  watch,
  onMounted,
  nextTick,
  provide,
  getCurrentInstance,
} from "vue";
import transformContent from "./util/transform-content";
import _set from "lodash.set";
import _isequal from "lodash.isequal";
import _clonedeep from "lodash.clonedeep";
import {
  collect,
  mergeValue,
  transformOutputValue,
  transformInputValue,
  correctValue,
} from "./util/utils";
let GROUP = "group";
/**
 * inputFormat 让整个输入机制复杂了很多。value 有以下输入路径:
 * 1. 传入的 form => inputFormat 处理
 * 2. updateForm => inputFormat 处理
 * 3. 但 content 中的 default 没法经过 inputFormat 处理,因为 inputFormat 要接受整个 value 作为参数
 * 4. 组件内部更新 value,不需要走 inputFormat
 */
let value = reactive({}); // 表单数据对象
let options = reactive({});
let initValue = reactive({});
let myelForm = ref();
let methods = {};
const customComponent = ref([]);
let emit = defineEmits(["update:FormData"]);
// 注入 element ui form 方法
/**
 * 与 element 相同,在 mounted 阶段存储 initValue
 * @see https://github.com/ElemeFE/element/blob/6ec5f8e900ff698cf30e9479d692784af836a108/packages/form/src/form-item.vue#L304
 */
onMounted(async () => {
  initValue = _clonedeep(value);
  await nextTick();
  // 检查 myelForm 是否已经初始化
  if (myelForm && myelForm.value) {
    Object.keys(myelForm.value).forEach((item) => {
      // 检查属性是否存在于 methods 对象中
      if (myelForm.value[item] && !(item in methods)) {
        methods[item] = myelForm.value[item];
      }
    });
  }
  /**
   * 有些组件会 created 阶段更新初始值为合法值,这会触发 validate。目前已知的情况有:
   * - el-select 开启 multiple 时,会更新初始值 undefined 为 []
   * @hack
   */
  methods.clearValidate();
});

let props = defineProps({
  //表单项
  content: {
    type: Array,
    required: true,
  },
  // 禁用
  disabled: {
    type: [Boolean, Function],
    default: false,
  },
  //只读
  readonly: {
    type: Boolean,
    default: false,
  },
  /**
   * v-model 的值。传入后会优先使用
   */
  FormData: {
    type: Object,
    default: undefined,
  },
});
//兼容处理
let innerContent = computed(() => transformContent(props.content));
// 初始化默认值
let setValueFromModel = () => {
  if (innerContent.length) return;
  /**
   * 没使用 v-model 时才从 default 采集数据
   * default 值没法考虑 inputFormat
   * 参考 value-format.md 的案例。那种情况下,default 该传什么?
   */
  let newValue = props.FormData
    ? transformInputValue(props.FormData, innerContent.value)
    : collect(innerContent.value, "default");
  correctValue(newValue, innerContent.value);
  if (!_isequal(value, newValue)) value = Object.assign(value, newValue);
};
// v-model初始化默认数据
watch(
  () => props.FormData,
  (newForm) => {
    if (!newForm) return;
    setValueFromModel();
  },
  { immediate: true, deep: true }
);
// 初始化默认数据
watch(
  innerContent,
  (newContent) => {
    try {
      if (!newContent) return;

      // 如果 content 没有变动 remote 的部分,这里需要保留之前 remote 注入的 options
      Object.assign(options, collect(newContent, "options"));
      setValueFromModel();
    } catch (error) {
      console.log(error);
    }
  },
  { immediate: true }
);

// v-model 传递值
watch(value, (newValue, oldValue) => {
  try {
    if (!newValue) return;
    if (props.FormData) {
      let data = Object.assign(
        props.FormData,
        transformOutputValue(newValue, innerContent)
      );
      emit("update:FormData", data);
    }
  } catch (error) {
    console.log(error, "-----");
  }
  // deep: true, // updateValue 是全量更新,所以不用
});

/**
 * 更新表单数据
 * @param  {String} options.id 表单ID
 * @param  {All} options.value 表单数据
 */
let updateValue = ({ id, value: v }) => {
  value[id] = v;
};
/**
 * 重置表单为初始值
 *
 * @public
 */
let resetFields = async () => {
  /**
   * 之所以不用 el-form 的 resetFields 机制,有以下原因:
   * - el-form 的 resetFields 无视 el-form-renderer 的自定义组件
   * - el-form 的 resetFields 不会触发 input & change 事件,无法监听
   * - bug1: https://github.com/FEMessage/el-data-table/issues/176#issuecomment-587280825
   * - bug2:
   *   0. 建议先在监听器 watch.value 里 console.log(v.name, oldV.name)
   *   1. 打开 basic 示例
   *   2. 在 label 为 name 的输入框里输入 1,此时 log:'1' ''
   *   3. 点击 reset 按钮,此时 log 两条数据: '1' '1', '' ''
   *   4. 因为 _isequal(v, oldV),所以没有触发 v-model 更新
   */
  value = _clonedeep(initValue);
  await nextTick();
  methods.clearValidate();
};
/**
 * 当 strict 为 true 时,只返回设置的表单项的值, 过滤掉冗余字段, 更多请看 update-form 示例
 * @param {{strict: Boolean}} 默认 false
 * @return {object} key is item's id, value is item's value
 * @public
 */
let getFormValue = ({ strict = false } = {}) => {
  return transformOutputValue(value, innerContent, { strict });
};
/**
 * update form values
 * @param {object} newValue - key is item's id, value is the new value
 * @public
 */
let updateForm = (newValue) => {
  newValue = transformInputValue(newValue, innerContent);
  mergeValue(value, newValue, innerContent);
};
/**
 * update select options
 * @param {string} id<br>
 * @param {array} options
 * @public
 */
let setOptions = (id, O) => {
  _set(options, id, O);
  options = Object.assign(options); // 设置之前不存在的 options 时需要重新设置响应式更新
};

/**
 * get custom component
 * @param {string} id<br>
 * @public
 */
const getComponentById = (id) => {
  let content = [];
  props.content.forEach((item) => {
    if (item.type === GROUP) {
      const items = item.items.map((formItem) => {
        formItem.groupId = item.id;
        return formItem;
      });
      content.push(...items);
    } else {
      content.push(item);
    }
  });
  const itemContent = content.find((item) => item.id === id);
  if (!itemContent) {
    return undefined;
  }
  if (!itemContent.groupId) {
    return customComponent.value[id].customComponent;
  } else {
    const componentRef = customComponent.value[itemContent.groupId].customComponent;
    return componentRef[`formItem-${id}`].customComponent;
  }
};
provide("methods", methods);
provide("updateForm", updateForm);
provide("setOptions", setOptions);
defineExpose({
  updateValue,
  resetFields,
  getFormValue,
  updateForm,
  setOptions,
  methods,
  getComponentById,
});
</script>
<script>
export default {
  name: "ElFormRenderer",
};
</script>

导出组件

src 根目录中创建index.js文件,代码如下:

import elFormRenderer from "./components/femessage/el-form-renderer.vue"; // 引入封装好的组件
export { elFormRenderer }; //实现按需引入*
const coms = [elFormRenderer]; // 将来如果有其它组件,都可以写到这个数组里

const components = [elFormRenderer];
const install = function (App, options) {
  components.forEach((component) => {
    App.component(component.name, component);
  });
};
export default { install }; // 批量的引入*

使用vite构建

编辑vite.config.js文件,新增build属性 vite中文文档https://cn.vitejs.dev/guide/build.html#library-mode

  1. build:这是一个包含构建选项的对象。

  2. lib:这是构建库的选项。

    • entry:指定了库的入口文件,通常是一个 JavaScript 文件的路径。
    • name:定义了库的名称,这里是 "el-form-renderer-vue3"。
    • fileName:定义了输出文件的命名规则,使用了一个函数来生成不同格式的文件名。
  3. rollupOptions:这是用于 Rollup 构建工具的选项,Rollup 通常用于打包 JavaScript 库。

    • external:指定了需要排除的外部依赖项,这里只有 "vue"。

    • output:定义了输出选项。

  • globals:在 UMD 构建模式下,指定了外部依赖的全局变量名,这里将 "vue" 映射到 "Vue",以确保在使用库时可以访问到 Vue.js。

    import { defineConfig } from "vite";
    import vue from "@vitejs/plugin-vue";
    import path from "path";

    // https://vitejs.dev/config/
    export default defineConfig({
    plugins: [vue()],
    build: {
    lib: {
    entry: path.resolve(__dirname, "src/index.js"),
    name: "el-form-renderer-vue3",
    fileName: (format) => el-form-renderer-vue3.${format}.js,
    },
    rollupOptions: {
    // 确保外部化处理那些你不想打包进库的依赖
    external: ["vue"],
    output: {
    // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
    globals: {
    vue: "Vue",
    },
    },
    },
    },
    });

修改package.json文件

{
  "name": "el-form-renderer-vue3",
  "version": "1.0.1",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "files": [
    "dist"
  ],
  "main": "./dist/el-form-renderer-vue3.umd.js",
  "module": "./dist/el-form-renderer-vue3.es.js",
  "exports": {
    ".": {
      "import": "./dist/el-form-renderer-vue3.es.js",
      "require": "./dist/el-form-renderer-vue3.umd.js"
    }
  },
  "dependencies": {
    "@element-plus/icons-vue": "^2.1.0",
    "axios": "^1.5.1",
    "element-plus": "^2.3.14",
    "lodash.clonedeep": "^4.5.0",
    "lodash.frompairs": "^4.0.1",
    "lodash.get": "^4.4.2",
    "lodash.has": "^4.5.2",
    "lodash.includes": "^4.3.0",
    "lodash.isequal": "^4.5.0",
    "lodash.isplainobject": "^4.0.6",
    "lodash.kebabcase": "^4.1.1",
    "lodash.set": "^4.3.2",
    "lodash.topairs": "^4.3.0",
    "vue": "^3.3.4",
    "vue-router": "4"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.2.3",
    "vite": "^4.4.5"
  }
}

打包

当我们都配置好以后,我们就要打包了,这是我们要上传得文件

打包,生成dist文件

npm run build

注册npm账号 官网地址

想要发布到npm仓库,就必须要有一个账号,先去npm官网注册一个账号,注意记住用户名、密码和邮箱,发布的时候可能会用到

有些小伙伴可能本地的npm镜像源采用的是淘宝镜像源或者其它的,如果想要发布npm包,我们得吧我们得npm源切换为官方得源,命令如下:

npm config set registry=https://registry.npmjs.org

发布前准备

dist文件生成package.json文件,自定义组件名(唯一,重名报错重新起一个就行),版本号每次上传要高于前一次版本号

dist根目录中运行:

npm init -y

{
  "name": "el-form-renderer-vue3",
  "version": "1.0.2",
  "description": "",
  "main": "el-form-renderer-vue3.es.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

登录npm用户

npm login
  • Username 用户名
  • Password 密码
  • Email 邮箱地址
  • Enter one-time password 验证码

执行发布命令

npm publish

已经上传成功

使用组件

当我们要在项目中使用的时候就复制 npm i el-form-renderer-vue3

package.json文件中就有了我们安装的组件

这个时候只要像element ui 那样引入就可以全局使用了,在main.js中引入

在我们要用到得.vue中使用

发布包

创建npm账户

如果你还没有npm账户,你需要先创建一个。前往npm官方网站注册一个账户。

注意:记住你填写的用户名、邮箱、密码,等下你在本地是需要用这些信息登录的。

登录npm

在本地登录你刚刚注册的账号

如果是npm,最后还会给你的邮箱发个验证码,填上之后再回车,才算真正登录成功。

执行 npm login,输入用户名、密码以及你注册时的邮箱。

使用以下命令在终端中登录到你的npm账户:

npm login

这将提示你输入你的npm用户名、密码和邮箱地址。

切换npm源

因为我们要发布到官方源上面,所以要确保源地址为官方地址 http://registry.npmjs.orghttps://registry.npmjs.com

可以通过 npm config get registry 命令查看当前 registry 源。

推荐使用nrm管理本地npm源。

全局安装 nrmnpm install -g nrm
查看npm源:nrm ls

会把所有的源都列出来,其中带*的是当前使用的源
添加npm源:nrm add xxx http://xxxnpm.cn/

xxx 是你给这个源起的名字,后面跟上源的URL
删除源:nrm del xxx

xxx 是你给这个源起的名字

发布到npm

最后,运行以下命令来发布你的Vue 3组件到npm:

npm publish

package.json

创建一个Node.js项目时,package.json文件是非常重要的,它包含了项目的配置信息和依赖项。下面是一个典型的package.json文件及其各个字段的介绍:

package.json字段

  • name: 项目的名称,应该是唯一的。
  • version: 项目的版本号,遵循语义化版本规则。
  • description: 项目的简要描述。
  • main: 指定项目的入口文件。
  • scripts: 自定义命令脚本,例如,你可以运行 npm start 启动应用。
  • keywords: 一组关键字,有助于其他人找到你的项目。
  • author: 作者的信息。
  • license: 项目的许可证,常见的包括 MIT、Apache-2.0、GPL-3.0 等。
  • repository: 指定项目的代码仓库信息,包括仓库类型和URL。
  • bugs: 定义问题跟踪系统的URL,以及可选的问题报告邮箱。
  • homepage: 项目的主页URL,通常是项目在代码托管平台上的页面。

接下来是依赖项:

  • dependencies: 生产依赖项,这些包在生产环境中需要。

  • devDependencies: 开发依赖项,这些包在开发和测试过程中需要,但不会包含在生产环境中。

    {
    "name": "my-node-app",
    "version": "1.0.0",
    "description": "My Node.js Application",
    "main": "index.js",
    "scripts": {
    "start": "node index.js",
    "test": "mocha"
    },
    "keywords": ["node", "javascript"],
    "author": "Your Name",
    "license": "MIT",

    "repository": {
      "type": "git",            // 仓库类型
      "url": "https://github.com/yourusername/my-node-app.git" // 仓库 URL
    },
    
    "bugs": {
      "url": "https://github.com/yourusername/my-node-app/issues", // 问题跟踪系统 URL
      "email": "youremail@example.com"  // 可选的问题报告邮箱
    },
    
    "homepage": "https://github.com/yourusername/my-node-app", // 项目主页 URL
    
    "dependencies": {
      "express": "^4.17.1",
      "body-parser": "^1.19.0"
    },
    "devDependencies": {
      "mocha": "^8.0.1",
      "chai": "^4.2.0"
    }
    

    }

要发布一个npm 包,name 和 version 字段是必填的;

包名(name 字段)命名规则:

  • 包名长度不能超过 214 个字符(命名空间也算在里面);
  • 包名所有字符必须小写;
  • 包名可以由连字符 - 组成;
  • 包名不能包含空格,不能以 . 或者 _ 开头,不能包含 ~)('!* 中的任意一个字符;
  • 包名不能包含任何非 url 安全字符(因为包名将作为 url 的一部分);
  • 包名不能与 Node.js / io.js 的核心模块、保留字或黑名单相同,例如 http。

版本号(version 字段)则需要遵循 semver 规范。

确定版本号

填写好 package.json 字段后,接下来就是确定我们要发布的版本号,每次对包的更改都应该对应一个版本。

版本号格式

格式:MAJOR.MINOR.PATCH ,值非负整数,且禁止在数字前面补 0

  • MAJOR:主版本号
  • MINOR:次版本号
  • PATCH::修订号

版本号递增逻辑

  • 当有破坏性不兼容的 API 变更时,升级主版本号
  • 当新增一些功能特性时,升级次版本号
  • 当做一些 bug 修复时,升级修订号

当某个版本还不稳定的时候,还可能要先发布一个先行版本,具体可看 semver 规范。

vite 打包库模式

当你开发面向浏览器的库时,你可能会将大部分时间花在该库的测试/演示页面上。在 Vite 中你可以使用 index.html 获得如丝般顺滑的开发体验。

当这个库要进行发布构建时,请使用 build.lib 配置项,以确保将那些你不想打包进库的依赖进行外部化处理,例如 vuereact

库模式配置

// vite.config.js
import { resolve } from 'path'
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    lib: {
      // 也可以是字典或多个入口点的数组
      entry: resolve(__dirname, 'lib/main.js'),
      name: 'MyLib',
      // the proper extensions will be added
      fileName: 'my-lib',
    },
    rollupOptions: {
      // 确保外部化处理那些你不想打包进库的依赖
      external: ['vue'],
      output: {
        // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
        globals: {
          vue: 'Vue',
        },
      },
    },
  },
})

入口文件将包含可以由你的包的用户导入的导出:

// lib/main.js
import Foo from './Foo.vue'
import Bar from './Bar.vue'
export { Foo, Bar }

使用如上配置运行 vite build 时,将会使用一套面向库的 Rollup 预设,并且将为该库提供两种构建格式:esumd (可在 build.lib 中配置):

$ vite build
building for production...
dist/my-lib.js      0.08 KiB / gzip: 0.07 KiB
dist/my-lib.umd.cjs 0.30 KiB / gzip: 0.16 KiB

荐在你库的 package.json 中使用如下格式:

{
  "name": "my-lib",
  "type": "module",
  "files": ["dist"],
  "main": "./dist/my-lib.umd.cjs",
  "module": "./dist/my-lib.js",
  "exports": {
    ".": {
      "import": "./dist/my-lib.js",
      "require": "./dist/my-lib.umd.cjs"
    }
  }
}

或者,如果暴露了多个入口起点:

{
  "name": "my-lib",
  "type": "module",
  "files": ["dist"],
  "main": "./dist/my-lib.cjs",
  "module": "./dist/my-lib.js",
  "exports": {
    ".": {
      "import": "./dist/my-lib.js",
      "require": "./dist/my-lib.cjs"
    },
    "./secondary": {
      "import": "./dist/secondary.js",
      "require": "./dist/secondary.cjs"
    }
  }
}

注意

如果 package.json 不包含 "type": "module",Vite 会生成不同的文件后缀名以兼容 Node.js。.js 会变为 .mjs.cjs 会变为 .js
环境变量

在库模式下,所有 import.meta.env.* 用法在构建生产时都会被静态替换。但是,process.env.* 的用法不会被替换,所以你的库的使用者可以动态地更改它。如果不想允许他们这样做,你可以使用 define: { 'process.env.NODE_ENV': '"production"' } 例如静态替换它们。

build配置项

build.lib

  • 类型: { entry: string | string[] | { [entryAlias: string]: string }, name?: string, formats?: ('es' | 'cjs' | 'umd' | 'iife')[], fileName?: string | ((format: ModuleFormat, entryName: string) => string) }

构建为库。

  1. entry 是必需的,因为库不能使用 HTML 作为入口。
  2. name 则是暴露的全局变量,并且在 formats 包含 'umd''iife' 时是必需的。默认 formats['es', 'umd'],如果使用了多个配置入口,则是 ['es', 'cjs']
  3. fileName 是输出的包文件名,默认 fileNamepackage.jsonname 选项,同时,它还可以被定义为参数为 formatentryAlias 的函数。

build.rollupOptions

自定义底层的 Rollup 打包配置。这与从 Rollup 配置文件导出的选项相同,并将与 Vite 的内部 Rollup 选项合并。查看 Rollup 选项文档 获取更多细节。

相关推荐
WebGIS皮卡茂3 天前
【数据可视化】Arcgis api4.x 热力图、时间动态热力图、timeSlider时间滑块控件应用 (超详细、附免费教学数据、收藏!)
javascript·vue.js·arcgis·信息可视化
中科GIS地理信息培训3 天前
【技术文章】ArcGIS Pro如何批量导出符号和工程样式?
arcgis
Z_W_H_3 天前
【ArcGISProSDK】初识
arcgis·arcgisprosdk
WebGIS皮卡茂3 天前
【数据可视化】Arcgis api 4.x 专题图制作之分级色彩,采用自然间断法(使用simple-statistics JS数学统计库生成自然间断点)
javascript·arcgis·信息可视化·前端框架
xa138508694 天前
ARCGIS PRO DSK MapTool
arcgis
依晴无旧6 天前
Hexo框架学习——从安装到配置
arcgis
Z_W_H_6 天前
【ArcGIS Pro】扩展模块 Nuget 使用
arcgis·sdk·arcgisprosdk·arcgispro
科研online6 天前
ArcGIS属性表汉字转拼音
arcgis
SteveJi6667 天前
CesiumJS+SuperMap3D.js混用实现可视域分析 S3M图层加载 裁剪区域绘制
前端·javascript·3d·arcgis
SteveJi6667 天前
CesiumJS+SuperMap3D.js混用实现通视分析
前端·javascript·3d·arcgis