vue2+js 实现自己的插件

21 阅读6分钟

随着前端技术的不断发展,Vue.js 已经成为了一个非常流行的 JavaScript 框架,它使得开发者能够更高效地构建用户界面。在 Vue.js 的生态系统中,插件扮演着重要的角色,它们能够扩展框架的功能,提高开发效率。对于开发者来说,掌握如何创建和使用插件是非常重要的,本文将介绍如何基于 Vue2+JS 实现自己的插件。

需要实现的命令:
  • 创建组件: yarn add:component 组件名称 组件中文名
  • 删除组件: yarn del:component 组件名称
  • 构建国际化: yarn build:lang
  • 构建样式: yarn build:style
  • 构建导出文件: yarn build:entry
    • 执行完 yarn add:component 或 yarn del:component 后要执行 yarn build:entry 命令

1. 初始化项目

使用 vue/cli 3 进行初始化项目,执行命令:vue3 create qiuer-resource-vue2-js (为什么是vue3,可以参考: (超详细)在同一设备下安装不同版本的vue脚手架笔记

D:\dataGitMy\qiuer\qiankun-all\workspaces>vue3 create qiuer-resource-vue2-js


Vue CLI v3.11.0
┌───────────────────────────┐
│  Update available: 5.0.8  │
└───────────────────────────┘
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-processors, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with dart-sass)
? Pick a linter / formatter config: Basic
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No


Vue CLI v3.11.0
✨  Creating project in D:\dataGitMy\qiuer\qiankun-all\workspaces\qiuer-resource-vue2-js.
⚙  Installing CLI plugins. This might take a while...

yarn install v1.22.22
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...

success Saved lockfile.
Done in 16.27s.
🚀  Invoking generators...
📦  Installing additional dependencies...

yarn install v1.22.22
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
Done in 9.29s.
⚓  Running completion hooks...

📄  Generating README.md...

🎉  Successfully created project qiuer-resource-vue2-js.
👉  Get started with the following commands:

 $ cd qiuer-resource-vue2-js
 $ yarn serve

生成的目录结构如下:


├── node_modules
├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   ├── views
│   │   ├── About.vue
│   │   └── Home.vue
│   ├── App.vue
│   ├── main.js
│   ├── router.js
│   └── store.js
├── .browserslistrc
├── .eslintrc.js
├── .gitignore
├── babel.config.js
├── package.json
├── postcss.config.js
├── README.md
└── yarn.lock

执行: yarn add element-ui 安装 element-ui 依赖

2. 文件操作的工具方法 ./build/utils/ToolsFile.js

安装相关依赖,执行命令: yarn add uppercamelcase file-save json-templater -D

/**
 * @description: 文件和文件夹操作
 */
const fs = require('fs');
const path = require('path');
const fsPromises = fs.promises;

// 同步判断目录是否存在
function dirIsExistsSync(dirPath){
  if (fs.existsSync(dirPath)) {
    console.log(`${dirPath} 目录存在`);
    return true;
  } else {
    console.log(`${dirPath} 目录不存在`);
    return false;
  }
}
// 同步创建文件夹
function createFolderSync (folderPath) {
  try {
    fs.mkdirSync(folderPath, { recursive: true });
    console.log(`${folderPath} 同步创建文件夹成功!`);
    return true;
  } catch (err) {
    console.error(`${folderPath} 同步创建文件夹失败: `, err);
    return false;
  }
}

module.exports = {
  /**
   * 判断目录是否存在-同步
   * 同步判断目录是否存在
   * directoryPath : 要判断的目录
   * 使用示例: const a = dirIsExistsSync('./examples1/c.txt')
   */
  dirIsExistsSync: (directoryPath) => {
    return dirIsExistsSync(directoryPath)
  },
  /**
   * 创建文件-同步
   * 创建文件, 同步创建并写入文件内容
   * filePath : 要创建的文件
   * content : 文件内容
   * 使用示例: writeFileSync('./examples/demo/d.md', '# Hello, \n this is a new file!')
   */
  writeFileSync(filePath, content){
    const lastDirName = filePath.split('/').reverse()[0];
    const isFile = lastDirName.lastIndexOf('.'); // 用于判断是否是文件
    if(isFile === -1) {
      console.log(`${filePath} 不是文件路径`);
      return false;
    }
    const fileIsExists = dirIsExistsSync(filePath) // 文件是否存在
    
    if(!fileIsExists) {
      const dir = filePath.substring(0, filePath.lastIndexOf('/') > -1 ? filePath.lastIndexOf('/') : filePath.lastIndexOf('\\'));
      const dirIsExists = dirIsExistsSync(dir) // 文件夹是否存在
      if(dir && !dirIsExists) {
        createFolderSync(dir)
      }
    }
    try {
      // 创建并写入文件内容
      fs.writeFileSync(filePath, content);
      console.log(`${filePath} 文件创建成功`);
      return true;
    } catch (err) {
      console.error(`${filePath} 创建文件失败:`, err);
      return false;
    }
  },
  /**
   * 创建文件夹-同步
   * folderPath : 要创建的文件夹路径
   * 使用示例: createFolderSync('./examples')
   */
  createFolderSync: (folderPath) => {
    try {
      fs.mkdirSync(folderPath, { recursive: true });
      console.log(`${folderPath} 同步创建文件夹成功!`);
      return true;
    } catch (err) {
      console.error(`${folderPath} 同步创建文件夹失败: `, err);
      return false;
    }
  },
  /**
   * 文件内容覆写-同步
   * 覆写文件,覆盖整个文件内容-同步
   * filePath : 要操作的文件路径
   * content : 要新写的内容
   * 使用示例: fileRewriteSync('./test/a.txt', '这是要追加的内容 \n')
   */
  fileRewriteSync: (filePath, content) => {
    try {
      fs.writeFileSync(filePath, content);
      console.log(`${filePath} 内容覆盖成功`);
      return true;
    } catch (err) {
      console.error(`${filePath} 覆盖内容失败:`, err);
      return false;
    }
  },
  /**
   * 文件内容追加-同步
   * 在已存在的文件中追加内容-同步
   * filePath : 要操作的文件路径
   * content : 要追加的内容
   * 使用示例: fileAppendContentSync('./test/a.txt', '这是要追加的内容 \n')
   */
  fileAppendContentSync: (filePath, content) => {
    try {
      fs.appendFileSync(filePath, content + '\n');
      console.log(`${filePath} 内容追加成功`);
      return true;
    } catch (err) {
      console.error(`${filePath} 追加内容失败:`, err);
      return false;
    }
  },
  /**
   * 删除文件/文件夹
   * @param {string} dirPath - 要删除的路径
   * @returns
   */
  delFileFolder: (dirPath) => {
    // 判断路径是否存在
    if(!fs.existsSync(dirPath)) {
      console.log(`${dirPath} 路径不存在`);
      return;
    }
    // 判断路径是否为文件
    let isFile = false;
    try {
      const stats = fs.statSync(dirPath);
      isFile = !!stats.isFile();
    } catch (err) {
      if (err.code === 'ENOENT') {
        // 文件或文件夹不存在
        console.error(`路径 ${dirPath} 不存在`);
        isFile = false;
      }
      throw err; // 抛出其他未知错误
    }
    if(isFile) {
      try {
        fs.unlinkSync(dirPath);
        console.log(`文件 ${dirPath} 已经被成功删除.`);
      } catch (err) {
        console.error(`删除文件 ${dirPath} 失败:`, err);
      }
      return;
    }
    // 同步删除文件夹
    function delFolder(delPath){
      try {
        const files = fs.readdirSync(delPath);
        files.forEach(file => {
          const curPath = path.join(delPath, file);
          const stats = fs.statSync(curPath);
          if (stats.isDirectory()) {
            // 递归删除子文件夹
            delFolder(curPath);
          } else {
            // 删除文件
            fs.unlinkSync(curPath);
          }
        });
        // 删除空文件夹
        fs.rmdirSync(delPath);
        console.log(`文件夹 ${delPath} 及其内容已被删除.`);
      } catch (err) {
        if (err.code === 'ENOENT') {
          console.error(`路径 ${delPath} 不存在`);
        } else {
          throw err;
        }
      }
    }
    delFolder(dirPath)
  },
  deleteFolder: async (folderPath) => {
    async function fn(dirPath){
      if(!dirIsExistsSync(dirPath)) {
        console.log(`${dirPath} 目录不存在`);
        return;
      }
      try {
        // 获取文件夹中的所有文件和子文件夹
        const files = await fs.promises.readdir(dirPath, { withFileTypes: true });
  
        // 递归删除每个文件或子文件夹
        for (const file of files) {
          const filePath = path.join(dirPath, file.name);
  
          if (file.isDirectory()) {
            // 如果是文件夹,递归删除
            await fn(filePath);
          } else {
            // 如果是文件,直接删除
            await fs.promises.unlink(filePath);
          }
        }
        // 删除空文件夹
        await fs.promises.rmdir(dirPath);
        console.log(`${dirPath} 异步删除文件夹成功!`);
      } catch (err) {
        console.error(`${dirPath} 删除文件夹失败:`, err);
        throw err;
      }
    }
    fn(folderPath);
  },
  /**
   * 文件内容修改-异步
   * 修改文件中指定内容
   * filePath : 要操作的文件路径
   * oldContent : 要替换的字符串
   * newContent : 新的字符串
   * 使用示例: replaceFileContent('./test/a.txt', '哈哈', '嘿嘿')
   */
  replaceFileContent(filePath, oldContent, newContent){
    fs.readFile(filePath, (err, data) => {
      if(err) throw err;
      let fileContent = data.toString();
      let modifiedContent = fileContent.replace(oldContent, newContent)
      fs.writeFile(filePath, modifiedContent, (e) => {
        if(e) throw e;
        console.log(`文件修改成功!`);
      })
    })
  },
  /**
   * 获取指定文件夹下的文件列表-同步
   * dir : 路径
   * cb : 回调函数
   */
  getDirFilesListSync: (dir, cb) => {
    const arr = [];
    const files = fs.readdirSync(dir);
    files.forEach(filename => { 
      const filedir = path.join(dir, filename);
      if(fs.statSync(filedir).isFile()) {
        arr.push(filename)
      }
    }); 
    cb(arr)
  },
}

3. 新增/删除组件时所要处理的文件配置 ./build/utils/newDelCompFiles.js

/**
 * 新增/删除 组件是要新增/删除的文件配置
 */

const path = require('path')

function getFilesConfig(CompName, componentName) {
  return [
    {
      filename: 'index.js',
      content: `import ${CompName} from './src/index';
  /* istanbul ignore next*/
  ${CompName}.install = function(Vue) {
    Vue.component(${CompName}.name, ${CompName});
  };
  
  export default ${CompName};
      `
    },
    {
      filename: 'readme.md',
      content: `# ${CompName}
  组件介绍
      `
    },
    {
      filename: 'src/index.vue',
      content: `<template>
  <div class="${camelCaseToHyphen(componentName)}">
    ${componentName}
  </div>
</template>
<script>
export default {
  name: '${CompName}',
  components: {},
  props: {},
  provide: function(){
    return {};
  },
  inject: [],
  data(){
    return {};
  },
  computed: {},
  watch: {},
  created() {},
  mounted() {},
  beforeDestroy() {},
  methods: {},
}
</script>

<style lang="scss" scoped>
</style>
`,
    },
    {
      filename: 'src/initConfig.js',
      content: '',
    },
    {
      filename: 'styles/index.scss',
      content: '',
    },
    {
      filename: 'index.scss',
      content: `@import './styles/index.scss';`,
    },
    {
      filename: path.join('../../examples/template', `${componentName}/index.vue`),
      content: `<template>
  <div class="comp-example-container">
    <${CompName} />
  </div>
</template>

<script>
export default {
  name: '',
  components: {},
  props: {},
  provide: function(){
    return {};
  },
  inject: [],
  data(){
    return {};
  },
  computed: {},
  watch: {},
  created(){},
  mounted() {},
  beforeDestroy() {},
  methods: {},
}
</script>

<style lang='scss' scoped>
</style>
`,
    },
    {
      filename: path.join('../../src/i18ns/en/', `${componentName}.js`),
      content: `export default {
      ${componentName}: {},
      }`
    },
    {
      filename: path.join('../../src/i18ns/zh-CN/', `${componentName}.js`),
      content: `export default {
      ${componentName}: {},
      }`
    },
    {
      filename: path.join('../../src/services/', `${componentName}/API.js`),
      content: `import ComponentAPI from '../ComponentAPI.json';
const API = {};
export default API;
`,
    },
    {
      filename: path.join(`../../src/services/`, `${componentName}/index.js`),
      content: `import API from './API';
import HttpClientHelper from '../../utils/httpClientHelper';
class ${CompName}Service {}

export default new ${CompName}Service();
`,
    }
  ];
}

/**
 * [camelCaseToHyphen 将驼峰命名转换为连字符]
 * @param  {[string]} str [驼峰命名的字符串]
 * @return {[string]}     [连字符的字符串]
 */
function camelCaseToHyphen(str) {
  return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
}

module.exports = getFilesConfig;

4. 命令脚本文件 ./build/bin/new-comp.js

'use strict';
/**
 * 执行命令示例: yarn add:component testComp 测试组件
 *      yarn add:component 组件名称 组件中文名
 * new.js脚本主要做了下面几件事:
 *    1. 创建一些需要提前准备的目录
 *    2. 将新建的组件添加到 components.json
 *    3. 将新建的组件添加到 exposeComponents.json
 *    4. 根据配置在 packages 下创建相关的文件及文件夹
 *    5. 重写 service/index.js 文件
 */
// process.on() 方法可以监听进程事件
process.on('exit', () => {
  console.log('当进程要退出之前,会触发exit事件。');
})
/**
 * process.argv:返回一个数组
 *  其中包含当 Node.js 进程被启动时传入的命令行参数
 *  第一个元素是 process.execPath
 *  第二个元素是正被执行的 JavaScript 文件的路径
 *  其余的元素是任何额外的命令行参数
 */
if(!process.argv[2]) {
  console.log('参数:', process.argv);
  console.error('[组件名]必填 - Please enter new component name');
  process.exit(1);
}

const path = require('path')
const uppercamelcase = require('uppercamelcase')
const componentName = process.argv[2]
const ComponentName = uppercamelcase(componentName);
const componentCnName = process.argv[3] || componentName
const PackagePath = path.resolve(__dirname, '../../packages', componentName)
const ToolsFile = require('../utils/ToolsFile');

// 要创建的文件
const getFilesConfig = require('../utils/newDelCompFiles')
const Files = getFilesConfig(ComponentName, componentName);
/******** 需要提前准备的一些文件或目录 start ********/ 
const prepareDirs = [
  './exposeComponents.json',
  './components.json',
  './examples/template',
  './packages',
  './src/i18ns/en',
  './src/i18ns/zh-CN',
  './src/services',
  './src/services/index.js',
];
prepareDirs.forEach(dir => {
  const lastDirName = dir.split('/').reverse()[0];
  const isFile = lastDirName.lastIndexOf('.'); // 用于判断是否是文件
  const isExists = ToolsFile.dirIsExistsSync(dir); // 目录是否存在
  if(!isExists) {
    if(isFile > -1) {
      const fileSuffix = lastDirName.substr(isFile+1)
      const fileContent = fileSuffix === 'json' ? '{}' : '';
      ToolsFile.writeFileSync(dir, fileContent)
    } else {
      ToolsFile.createFolderSync(dir)
    }
  }
});

console.log('目录准备完成!')
/******** 需要提前准备的一些文件或目录 end ********/ 

/******** 添加到 components.json start ********/ 
const componentsFile = require('../../components.json');
if(componentsFile[componentName]) {
  console.error(`${componentName} 已存在`)
  process.exit(1);
}
componentsFile[componentName] = {
  url: `./packages/${componentName}/index.js`,
  alias: componentCnName,
};
ToolsFile.fileRewriteSync(path.join(__dirname, '../../components.json'), JSON.stringify(componentsFile, null, '  '));
console.log('添加到 components.json 完成!')
/******** 添加到 components.json end ********/ 

/******** 添加到 exposeComponents.json start ********/ 
const exposesFile = require('../../exposeComponents.json');
exposesFile[`./${componentName}`] = `./packages/${componentName}/src/index.vue`;
ToolsFile.fileRewriteSync(path.join(__dirname, '../../exposeComponents.json'), JSON.stringify(exposesFile, null, '  '));
console.log('添加到 exposeComponents.json 成功');
/******** 添加到 exposeComponents.json end ********/ 

/******** 在 packages 下创建相关文件 start ********/ 
Files.forEach(file => {
  ToolsFile.writeFileSync(path.join(PackagePath, file.filename), file.content);
})
console.log('packages DONE!')
/******** 在 packages 下创建相关文件 end ********/ 

/******** services/index.js start ********/ 
ToolsFile.fileAppendContentSync(
  path.join(__dirname, '../../src/services/index.js'),
  `export { default as ${uppercamelcase(ComponentName)}Service } from './${componentName}'; \n`
);
/******** services/index.js end ********/ 

5. 命令脚本文件 ./build/bin/del-comp.js

'use strict';
/**
 * 执行命令示例: yarn del:component testComp
 *      yarn del:component 组件名称
 */

// process.on() 方法可以监听进程事件
process.on('exit', () => {
  console.log('当进程要退出之前,会触发exit事件。');
})

/**
 * process.argv:返回一个数组
 *  其中包含当 Node.js 进程被启动时传入的命令行参数
 *  第一个元素是 process.execPath
 *  第二个元素是正被执行的 JavaScript 文件的路径
 *  其余的元素是任何额外的命令行参数
 */
if(!process.argv[2]) {
  console.error('[要删除的组件名]必填 - Please enter new component name');
  process.exit(1);
}

const path = require('path')
const fs = require('fs')
const fileSave = require('file-save')
const uppercamelcase = require('uppercamelcase')
const componentName = process.argv[2]
const ComponentName = uppercamelcase(componentName);
const componentCnName = process.argv[3] || componentName
const PackagePath = path.resolve(__dirname, '../../packages', componentName)
const ToolsFile = require('../utils/ToolsFile');

// 要删除的文件
const getFilesConfig = require('../utils/newDelCompFiles')
const Files = getFilesConfig(ComponentName, componentName);

/******** 删除 components.json 中指定的组件 start ********/ 
const componentsFile = require('../../components.json');
if(componentsFile[componentName]) {
  delete componentsFile[componentName];
}
ToolsFile.fileRewriteSync(path.join(__dirname, '../../components.json'), JSON.stringify(componentsFile, null, '  '));
console.log('components.json 操作完成!')
/******** 删除 components.json 中指定的组件 end ********/ 

/******** 删除 exposeComponents.json 中指定的组件 start ********/ 
const exposesFile = require('../../exposeComponents.json');
delete exposesFile[`./${componentName}`];
ToolsFile.fileRewriteSync(path.join(__dirname, '../../exposeComponents.json'), JSON.stringify(exposesFile, null, '  '));
console.log('exposeComponents.json 操作完成!')
/******** 删除 exposeComponents.json 中指定的组件 end ********/ 

// 删除相应文件
Files.forEach(file => {
  ToolsFile.delFileFolder(path.join(PackagePath, file.filename))
})
const delDirs = [
  `./examples/template/${componentName}`,
  PackagePath,
  `./src/services/${componentName}`,
];
delDirs.forEach(dir => {
  ToolsFile.deleteFolder(dir)
})
ToolsFile.replaceFileContent(
  path.join(__dirname, '../../src/services/index.js'),
  `export { default as ${uppercamelcase(ComponentName)}Service } from './${componentName}'; \n`,
  '',
)
console.log('完成删除组件操作!')

6. 命令脚本文件 ./build/bin/build-entry.js

var Components = require('../../components.json');
var fs = require('fs');
var render = require('json-templater/string');
var uppercamelcase = require('uppercamelcase');
var path = require('path');
var endOfLine = require('os').EOL;

/********************* 自定义指令 start **********************/
const ToolsFile = require('../utils/ToolsFile');
const includeDirectivesTmpl = [];
const vueUseDirectivesTmpl = [];
ToolsFile.getDirFilesListSync(path.resolve(__dirname, '../../src/directives'), (arr) => {
  arr.forEach(v => {
    const directive = v.substring(0, v.lastIndexOf('.'));
    includeDirectivesTmpl.push(render('import {{directive}} from \'./directives/{{filename}}\'', {
      directive: directive,
      filename: v,
    }))
    vueUseDirectivesTmpl.push(render('Vue.use({{directive}})', {
      directive: directive,
      filename: v,
    }))
  })
})

/********************* 自定义指令 end **********************/
var OUTPUT_PATH = path.join(__dirname, '../../src/index.js');
var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\'';
var INSTALL_COMPONENT_TEMPLATE = '  {{name}}';
var MAIN_TEMPLATE = `/* Automatically generated by './build/bin/build-entry.js' */
/*** 组件引入 start ***/
{{include}}
/*** 组件引入 end ***/

/*** 指令引入 start ***/
{{importDirectives}}
/*** 指令引入 end ***/

// import './styles/index.less'
// import QiuerUiLang from './i18ns/index' // 导出国际化文件
// import QiuerUiStore from './store/index' // 导出store
// import locale from './locale'
const components = [
{{install}}
]
const install = function (Vue, opts = {}) {
  // locale.i18n(opts.i18n)
  components.forEach(component => {
    Vue.component(component.name, component)
  })
  // 注册全局指令 start
  {{useDirectives}}
  // 注册全局指令 end
}
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}
export {
  install,
  // QiuerUiLang,
  // QiuerUiStore,
{{list}}
}

export default {
  version: '{{version}}',
  install,
  // locale: locale.use,
  // i18n: locale.i18n,
{{list}}
}
`;

delete Components.font;

var ComponentNames = Object.keys(Components);

var includeComponentTemplate = [];
var installTemplate = [];
var listTemplate = [];

ComponentNames.forEach(name => {
  var componentName = uppercamelcase(name);

  includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
    name: componentName,
    package: name
  }));

  if (['Loading', 'MessageBox', 'Notification', 'Message', 'InfiniteScroll'].indexOf(componentName) === -1) {
    installTemplate.push(render(INSTALL_COMPONENT_TEMPLATE, {
      name: componentName,
      component: name
    }));
  }

  if (componentName !== 'Loading') listTemplate.push(`  ${componentName}`);
});

var template = render(MAIN_TEMPLATE, {
  include: includeComponentTemplate.join(endOfLine),
  install: installTemplate.join(',' + endOfLine),
  version: process.env.VERSION || require('../../package.json').version,
  list: listTemplate.join(',' + endOfLine),
  importDirectives: includeDirectivesTmpl.join(endOfLine),
  useDirectives: vueUseDirectivesTmpl.join(endOfLine),
});

fs.writeFileSync(OUTPUT_PATH, template);
console.log('[build entry] DONE:', OUTPUT_PATH);

7. 命令脚本文件 ./build/bin/build-style.js

var Components = require('../../components.json');
var fs = require('fs');
var render = require('json-templater/string');
var path = require('path');
var endOfLine = require('os').EOL;

var OUTPUT_PATH = path.join(__dirname, '../../src/styles/components.less');
var IMPORT_TEMPLATE = '@import \'../../packages/{{package}}/index.less\';'; 
var MAIN_TEMPLATE = `/* Automatically generated by './build/bin/build-style.js' */
{{include}}
`;

delete Components.font;
function fileExists(filePath) {
  try {
    return fs.statSync(filePath).isFile();
  } catch (err) {
    return false;
  }
}
var ComponentNames = Object.keys(Components);

var includeComponentTemplate = [];
// includeComponentTemplate.push(render('@import \'../../src/styles/{{name}}.less\';', {
//   name: 'common'
// }));
ComponentNames.forEach(name => {
  var filePath = path.resolve(__dirname, "../../packages/" + name + "/index.less");
  if (fileExists(filePath)) { 
    includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
      package: name
    }));
  }
});

var template = render(MAIN_TEMPLATE, {
  include: includeComponentTemplate.join(endOfLine),
});

fs.writeFileSync(OUTPUT_PATH, template);

8. 命令脚本文件 ./build/bin/build-lang.js

var fs = require('fs');
var render = require('json-templater/string');
var path = require('path');
var endOfLine = require('os').EOL;

var OUTPUT_PATH = path.join(__dirname, '../../src/i18ns/index.js');

var IMPORT_TEMPLATE = `import deepmerge from 'deepmerge';
const lang = {};`;
var REQUIRE_TEMPLATE = `const {{ctxName}} = require.context('./{{langType}}', true, /\.ts$/);
lang['{{langType}}'] = {{ctxName}}.keys().reduce((total, key) => deepmerge(total, {{ctxName}}(key).default), {});`;
var MAIN_TEMPLATE = `/* Automatically generated by './build/bin/build-lang.js' */
{{include}}
export default lang;
`;


var filePath = path.resolve(__dirname, '../../src/i18ns');
var files = fs.readdirSync(filePath)
var langList = [];
files.forEach(function (item, index) {
  if (item !== 'index.js') {
    langList.push(item)
  }
})

var includeLangType = [];
includeLangType.push(IMPORT_TEMPLATE);
langList.forEach((item, index) => {
  includeLangType.push(render(REQUIRE_TEMPLATE, {
    ctxName: `ctx${index}`,
    langType: item
  }))
})

var template = render(MAIN_TEMPLATE, {
  include: includeLangType.join(endOfLine),
});

fs.writeFileSync(OUTPUT_PATH, template);

9. 将src相关的文件移动到 examples 文件夹下

10. 修改项目入口文件为 examples/main.js

修改 vue.config.js

    entry: {
      main: resolve('examples/main.js') // 修改入口文件
    },

11. 最终插件项目的目录结构

├── build
│   └── bin // 存放命令的脚本
│   │   ├── build-entry.js
│   │   ├── build-lang.js
│   │   ├── build-style.js
│   │   ├── del-comp.js
│   │   ├── new-comp.js
│   │   ├── newAndDelCompFiles.js
│   │   └── toolsFile.js
├── examples  // 组件的使用示例
│   ├── assets
│   │   └── common.less
│   ├── components
│   │   └── ...
│   ├── i18ns
│   │   ├── en
│   │   │   └── ...
│   │   ├── zh
│   │   │   └── ...
│   │   ├── index.js
│   │   └── local.js
│   ├── imgs
│   │   └── ...
│   ├── router
│   │   └── index.js
│   ├── template
│   │   └── ...
│   ├── utils
│   │   └── ...
│   ├── views
│   │   ├── errorPage
│   │   │   └── index.vue
│   │   ├── layout
│   │   │   ├── aside.vue
│   │   │   └── index.vue
│   │   ├── login
│   │   │   └── index.vue
│   │   └── index.vue
│   ├── App.vue
│   └── main.js
├── node_modules
├── packages  // 存放各个组件
│   ├── ...
│   └── ...
├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   ├── directives  // 存放要导出的指令
│   │   ├── ...
│   │   └── ...
│   ├── i18ns
│   │   ├── en
│   │   ├── zh-CN
│   │   └── index.js
│   ├── locale
│   │   ├── lang
│   │   │   ├── en.js
│   │   │   └── zh-CN.js
│   │   ├── format.js
│   │   └── index.js
│   ├── services
│   │   ├── ...
│   │   └── ...
│   ├── store
│   │   ├── ...
│   │   └── ...
│   ├── styles
│   │   ├── common.less
│   │   ├── components.less
│   │   ├── index.less
│   │   └── var.less
│   ├── utils
│   │   ├── ...
│   │   └── ...
│   ├── views
│   │   ├── About.vue
│   │   └── Home.vue
│   ├── App.vue
│   ├── main.js
│   ├── router.js
│   └── store.js
├── .browserslistrc
├── .eslintrc.js
├── .gitignore
├── babel.config.js
├── components.json
├── exposeComponents.json
├── package.json
├── postcss.config.js
├── README.md
├── vue.config.js
└── yarn.lock

12. examples 模块下的结构:


├── ...
├── examples
│   ├── assets
│   │   └── common.less
│   ├── components
│   │   └── ...
│   ├── i18ns
│   │   └── ...
│   ├── imgs
│   │   └── ...
│   ├── router
│   │   └── index.js
│   ├── template
│   │   └── ...
│   ├── utils
│   │   └── ...
│   ├── views
│   │   ├── errorPage
│   │   │   └── index.vue
│   │   ├── layout
│   │   │   ├── aside.vue
│   │   │   └── index.vue
│   │   ├── login
│   │   │   └── index.vue
│   │   └── index.vue
│   ├── App.vue
│   └── main.js
├── ...
└── ...