1. 约定优于配置
简单说就是用约定好的规则作为框架来写代码。在steam-game-ui
中一个每次要新加一个新的组件要做以下几件事:
- 往
packages
文件夹里面添加组件文件、文档和导出组件的index.js
- 在打包入口引入和导出新组件
- 往
examples
文件夹里面添加路由引入组件以及添加导航 - 往
tests
文件夹里面添加单元测试文件
它们的配置都是分散的,不熟悉项目结构的人容易忽略其中的某一个步骤,而且分散的配置也容易导致出错,时间一长每次添加组件的时候可能还得先查看一下代码逻辑。所以在steam-game-ui
中把部分配置单独提取出来,同时新增npm run new 组件名 [组件中文名]
命令初始化新的组件。
1.1. 创建新文件
一个新的组件需要新建以下文件:packages/组件/组件.vue
、packages/组件/index.js
、packages/组件/docs/index.md
、tests/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