1. 约定优于配置

简单说就是用约定好的规则作为框架来写代码。在steam-game-ui中一个每次要新加一个新的组件要做以下几件事:

  • packages文件夹里面添加组件文件、文档和导出组件的index.js
  • 在打包入口引入和导出新组件
  • examples文件夹里面添加路由引入组件以及添加导航
  • tests文件夹里面添加单元测试文件

它们的配置都是分散的,不熟悉项目结构的人容易忽略其中的某一个步骤,而且分散的配置也容易导致出错,时间一长每次添加组件的时候可能还得先查看一下代码逻辑。所以在steam-game-ui中把部分配置单独提取出来,同时新增npm run new 组件名 [组件中文名]命令初始化新的组件。

1.1. 创建新文件

一个新的组件需要新建以下文件:packages/组件/组件.vuepackages/组件/index.jspackages/组件/docs/index.mdtests/specs/组件.spec.js,他们的初始化模板都是共性的,我们只需要获取组件的名称和中文名称就可以生成了,这里拿packages/组件/组件.vue举个简单的例子👇

const Files = [{
    filename: path.join(`${ComponentName}.vue`),
    content: `<template>
                <div class="st-${componentname}">新组件</div>
            </template>
            <script>
                export default {
                    name: 'St${ComponentName}'
                };
            </script>`
}]
// 创建 package
Files.forEach(file => {
  fileSave(path.join(PackagePath, file.filename))
    .write(file.content, 'utf8')
    .end('\n');
});

我们写了一个Files的模板列表,然后每一个模板需要提供filename模板路径和content模板内容,然后遍历列表用fs生成对应的文件。其他的文件也是一样的,只需要往Files列表里面新增模板就可以了,具体的模板参考build/new.js里面的配置。

1.2. 自动引入组件

在打包入口的packages/index.js需要引用和导出组件,同时还需要导出一个install方法,在install中除了部分组件有指令或者服务的方式调用,其他的都是注册一下组件就可以了。所以我们可以把所有的组件列成一份组件清单components.json,然后根据这份组件清单和模板去自动生成packages/index.js,因为指令和服务调用的组件有限,所以这部分的调用我们还是写死在模板里面。这块的工作因为就是打包的过程,我放在build\build-entry.js里面,同时因为需要用到插值,所以这里使用了一个模板引擎,遍历components.json然后渲染模板,最后通过fs去生成文件。

components.json的自动生成过程和上面的生成文件一样

{
  "button": "packages/button/index.js",
  "icon": "packages/icon/index.js",
  "loading": "packages/loading/index.js",
  "message": "packages/message/index.js",
  "clickoutside": "packages/clickoutside/index.js"
}

build\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;

var OUTPUT_PATH = path.join(__dirname, './../packages/index.js');
var IMPORT_TEMPLATE = 'import {{name}} from \'packages/{{package}}/index.js\';';
var INSTALL_COMPONENT_TEMPLATE = '  {{name}}';
var MAIN_TEMPLATE = `/* Automatically generated by './build/gen-components-index.js' */
{{include}}
import 'src/style/icon.scss';

const components = [
{{install}}
];

const install = function (Vue, opts = {}) {
  components.forEach(component => {
    Vue.component(component.name, component);
  });
  Vue.use(Loading.directive);
  Vue.directive('clickoutside', Clickoutside);
  Vue.prototype.$message = Message;
  Vue.prototype.$loading = Loading.service;
};

if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue);
}

export default {
  version: '{{version}}',
  install,
{{list}}
};
`;
var componentNames = Object.keys(components);
var includeComponentTemplate = [];
var installTemplate = [];
var listTemplate = [];
componentNames.forEach(name => {
  let componentName = uppercamelcase(name);
  includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
    name: componentName,
    package: name
  }));
  if (['Loading', 'Message', 'Clickoutside'].indexOf(componentName) === -1) {
    installTemplate.push(render(INSTALL_COMPONENT_TEMPLATE, {
      name: componentName,
      component: name
    }));
  }
  if (['Loading', 'Message', 'Clickoutside', 'Icon'].indexOf(componentName) === -1) {
    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)
});

fs.writeFileSync(OUTPUT_PATH, template);

1.3. 配置导航

这一块也单独抽成一个通用配置nav.config.json,这里不仅有组件的导航也有一些公共的文档的导航,所以这里我们单独用一个components字段来维护组件的导航,在new新组件的时候就往components里面push新的组件。在官网里面只需要读取这份json然后添加到导航上就可以了。

{
    "guide": {
        "name": "开发指南",
        "children": [{
            "name": "快速上手",
            "path": "/quickstart",
            "docsPath": "examples/guide/quickstart.md"
        }, {
            "name": "参与开发",
            "path": "/develop",
            "docsPath": "examples/guide/develop.md"
        }]
    },
    "components": {
        "name": "组件",
        "children": [
            {
                "name": "Icon 图标",
                "path": "/icon",
                "docsPath": "packages/icon/docs/index.md"
            }, {
                "name": "Button 按钮",
                "path": "/button",
                "docsPath": "packages/button/docs/index.md"
            }
        ]
    }
}

新组件的添加逻辑👇

// 添加到 nav.config.json
const navConfigFile = require('./../examples/nav.config.json');
navConfigFile['components'].children.push({
  name: componentname !== chineseName ? `${ComponentName} ${chineseName}` : ComponentName,
  path: `/${componentname}`,
  docsPath: `packages/${componentname}/docs/index.md`
});

fileSave(path.join(__dirname, './../examples/nav.config.json'))
  .write(JSON.stringify(navConfigFile, null, '  '), 'utf8')
  .end('\n');

1.4. 配置路由

路由的配置同样也是依据nav.config.json这份配置,因为我们把模块的路径通过变量的方式提取出来了,所以这里还会涉及到一个关于依赖管理的知识点。我们知道webpack是静态编译,当require的路径有表达式的时候,编译阶段无法知道要引入哪些文件。但是webpack会根据表达式的内容区拆分成一个路径和文件名,然后把路径下的文件全部引入,当运行时的时候再去解析表达式,然后去匹配引入的文件。所以webpack能够支持动态地require,但会导致所有可能用到的模块都包含在bundle 中。但是通过require.context可以让我们手动创建一个符合我们自定义规则的引用上下文,这样就能够精准得获取到我们只需要用到的模块。语法如下

require.context(directory, useSubdirectories = false, regExp = /^\.\//);

项目内的具体配置如下👇

import Vue from 'vue';
import navConfig from './nav.config';
import Router from 'vue-router';
import Home from './views/Home.vue';
Vue.use(Router);

let navs = [];
const componentsContext = require.context('./../packages/', true, /\.md/);
const componentsContextKeys = componentsContext.keys();
const guideContext = require.context('./../examples/', true, /\.md/);
const guideContextKeys = guideContext.keys();
Object.keys(navConfig).forEach(group => {
  navConfig[group].children.forEach(nav => {
    if (nav.path && nav.docsPath) {
      let docsPath = nav.docsPath.replace(/examples|packages/, '.');
      let isGuide = guideContextKeys.indexOf(docsPath) > -1;
      let isComponent = componentsContextKeys.indexOf(docsPath) > -1;
      if (isGuide || isComponent) {
        navs.push({
          path: nav.path,
          component: isGuide ? guideContext(docsPath).default : componentsContext(docsPath).default
        });
      }
    }
  });
});
let router = {
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
      children: navs
    }
  ]
};
export default new Router(router);

1.5. 总结

经过上面的工作,我们就能够使用npm run new 组件名 [组件中文名]命令来初始化一个新的组件了,具体的代码查看build/new.js

powered by Gitbook该文件修订时间: 2019-09-12 00:10:40

results matching ""

    No results matching ""