TS + Vue:封装可发布组件

1,607 阅读2分钟

目标

封装一个表单组件,包括 输入框 Input、下拉选择框 Select、查询按钮 Button

ts-vue-query.png

实现

基础框架

因为我们要开发的是一个组件,而不是外部工程,所以我们在 TS + Vue:搭建开发环境 的基础上进行一些改造。

一、webpack:抽离入口文件

从公共配置(webpack.base.config.js)中抽离出入口文件配置,因为开发环境和生产环境需要不同的入口。

  • webpack.dev.config.js

    module.exports = {
      entry: "./src/index.ts",
    };
    
  • webpack.pro.config.js

    module.exports = {
      entry: "./src/main.ts",
    };
    

二、webpack:修改输出文件名

  • webpack.base.config.js

    module.exports = {
      output: {
        filename: "employee-query.js",
        clean: true,
      },
      // ... 其他配置
    };
    

三、webpack: HtmlWebpackPlugin

因为调试时,需要页面运行组件,所以将 HtmlWebpackPluginbase 移放到 dev

  • webpack.dev.config.js

    const HtmlWebpackPlugin = require("html-webpack-plugin");
    
    module.exports = {
      // ...其他配置
      plugins: [
        // ...plugins
        new HtmlWebpackPlugin({
          template: "./src/tpl/index.html",
        }),
      ],
    };
    

四、webpack:生产环境的配置

把组件打包成 umd 模块,安装 nodeExternals 插件。

nodeExternals:能够排除 node_modules 目录中所有模块,还提供一些选项,比如白名单 package(whitelist package)

  • 安装 webpack-node-externals
$ npm i -D webpack-node-externals
  • webpack.dev.config.js

    const nodeExternals = require("webpack-node-externals");
    
    module.exports = {
      entry: "./src/main.ts",
      output: {
        libraryTarget: "umd",
        library: "EmployeeQuery", // 库名,在全局环境下被挂载在window下
      },
      externals: [nodeExternals()],
    };
    

五、package.json:Library 的入口

package.json 中的 main 字段指向的是 Library 的入口,通常有 3 个选择:

  1. 指向源代码入口文件,如 src/index.js;
  2. 指向打包后的开发版本,如 dist/library.js;
  3. 指向打包后的发布版本,如 dist/library.min.js

这里我们将它指向打包后的开发版本。

  • package.json
{
  // ...
  "main": "./dist/employee-query.js"
}

编写组件

装饰器模式

为什么需要 vue-class-component

typescript 里写 vue 每次都需要写很多额外的形式代码:

而装饰器就是解决这些冗余代码的(实质上并没有减少,只是用一层函数包装了

$ npm i --save vue-property-decorator

components/EmployeeQuery.vue

v-model.trim:自动过滤用户输入的首尾空白字符, x! 将从 x 值域中排除 nullundefined

组件包含三个 Propname(员工姓名)、selected(部门选中值)、department(部门列表)

<template>
  <div class="employee-query">
    <input type="text" placeholder="姓名" v-model.trim="tempName" />
    <select v-model.number="tempSelected">
      <option value="0">部门</option>
      <option
        v-for="option in department"
        :value="option.departmentId"
        :key="option.departmentId"
      >
        {{ option.department }}
      </option>
    </select>
    <button @click="query">查询</button>
  </div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from "vue-property-decorator";

@Component
export default class EmployeeQuery extends Vue {
  @Prop({ type: String, default: "" })
  name?: string = "";

  @Prop({ type: Number, default: 0 })
  selected?: number = 0;

  @Prop({ type: Array, default: () => [] })
  department?: { department: string; departmentId: number }[];

  tempName: string = this.name!;
  tempSelected: number = this.selected!;

  query() {
    this.$emit("query", { name: this.tempName, selected: this.tempSelected });
  }
}
</script>
<style scoped>
.employee-query {
  display: flex;
}
input,
select {
  margin-right: 10px;
}
</style>

开发环境入口(index.ts)

index.ts 注册组件,传入数据,处理 query 事件。

  • src/index.ts

    import Vue from "vue";
    import EmployeeQuery from "./components/EmployeeQuery.vue";
    
    let app = new Vue({
      el: ".app",
      components: { EmployeeQuery },
      template: `<employee-query @query="getQuery" :department="department"/>`,
      data() {
        return {
          department: [
            { department: "技术部", departmentId: 1 },
            { department: "产品部", departmentId: 2 },
            { department: "市场部", departmentId: 3 },
            { department: "运营部", departmentId: 4 },
          ],
        };
      },
      methods: {
        getQuery(param: any) {
          console.log(param);
        },
      },
    });
    

localhost:8080 调试组件。

$ npm start

生产环境入口(main.ts)

我们只是用了一个单文件组件,所以直接导出就可。

  • src/main.ts

    import EmployeeQuery from "./components/EmployeeQuery.vue";
    export default EmployeeQuery;
    

build 后,检查是否生成 src/employee-query.js

npm run build

坑儿汇总

experimentalDecorators

ERROR: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning.

可以通过设置 tsconfig.json 中的 experimentalDecorators 来删除此警告。

  • tsconfig.json
{
  "compilerOptions": {
    // ...other options
    "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */
  }
}

DevTools failed to load SourceMap

⚠️ WARNING:DevTools failed to load SourceMap: Could not load content for webpack://vue-employee-query/node_modules/sockjs-client/dist/sockjs.js.map: HTTP error: status code 404, net::ERR_UNKNOWN_URL_SCHEME

ts-vue-webpack.png

webpack 5 中,可以用 devtool 选项控制是否生成,以及如何生成 source map。

  • 原 webpack.dev.config.js
const webpack = require("webpack");
module.exports = {
  plugins: [
    new webpack.LoaderOptionsPlugin({
      options: {
        devtools: "cheap-module-eval-source-map",
      },
    }),
  ],
};
  • 更改后
module.exports = {
  devtool: "eval-cheap-source-map",
};

@Prop

如果不设置默认值,Vue 会报:

⚠️ [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.

  • src/components/EmployeeQuery.vue
// ❌
@Prop(String)
name?: string = "";

@Prop(Number)
selected?: number = 0;

// ✅
@Prop({ type: String, default: "" })
name?: string = "";

@Prop({ type: Number, default: 0 })
selected?: number = 0;