最近搭建毕设前端框架的时候,每当建立一个页面,我都须要建立这个页面组件,建立它的Route,最后将该Route加入到总的Route。固然,这里的流程还不算复杂,基本也是复制粘贴改改变量,可是后面还要用到Redux,可能还会使用saga…再将它们加入这一条流程线,我须要改的东西又多了。html
在公司实习的脚手架里,发现有大佬造的轮子,以前也只是照着命令敲拿来用,此次顺带研究了一下核心功能,结合个人毕设框架须要,加入了最简单的自动化“脚本”。前端
Node环境下执行。node
命令映射,使用commander。react
让文件能够经过命令行的形式执行git
文件读写,这里我使用的是fs-extra,使用Node自带的File System,可是前者支持Promise和Async, Await。github
文件读写只是读取模板文件内容,而后写入到新的文件为咱们所用shell
模板字符串,使用lodash/string的模板字符串方法template。json
模板字符串:咱们可使用xxx.tmpl格式的文件存储咱们的模板,须要替换的内容使用ubuntu
<%= xxx %>
表示便可,下面会给出文件原型api
文件修改,使用ts-simple-ast。
文件修改则是直接修改原来文件,加入本身所需的东西,例如修改变量值,这也是这篇文章中提到的较为简单的一个用途,其余更复杂的也能够参考文档学习
每当新增一个页面,咱们须要建立一个基本框架组件,一个Route,最后把这个Route自动插入到总的Router里。
这里建立了一个很是简单的组件,带有Props和State,interface使用Ixxx命名。
import React from 'react';
interface I<%= featureUpperName %>Props {}
interface I<%= featureUpperName %>State {}
export default class <%= featureUpperName %> extends React.Component<I<%= featureUpperName %>Props, I<%= featureUpperName %>State> {
constructor(props: I<%= featureUpperName %>Props) {
super(props);
this.state = {};
}
render() {
return (
<h2>My Home</h2>
)
}
}
复制代码
这个文件里加入全部须要导出的Component,并做为统一导出出口。
export { default as <%= featureUpperName %> } from './<%= featureUpperName %>'
复制代码
自定义的Route,属性也基本遵循原生Route,加入loadable component,支持按需加载。
import App from '../common/component/App';
import { IRoute } from '@src/common/routeConfig';
const loader = (name: string) => async () => {
const entrance = await import('./');
return entrance[name];
};
const childRoutes: IRoute[] = [{
path: '/<%= featureName %>',
name: '<%= featureUpperName %>',
loader: loader('<%= featureUpperName %>'),
}]
export default {
path: '/<%= featureName %>',
name: '',
component: App,
childRoutes
};
复制代码
上面三个便做为基本模板文件,下面这个则是总的Route。
完成一个页面的建立并生成它的route后,须要在该文件引入这个route,而后修改变量childRoutes,插入该route,这样咱们的工做就算完成啦。
import HomeRoute from "../features/home/route";
export interface IRoute {
path: string
name: string
component?: React.ComponentClass | React.SFC;
childRoutes?: IRoute[]
loader?: AsyncComponentLoader
exact?: boolean
redirect?: string
}
const childRoutes: IRoute[] = [HomeRoute]
const routes = [{
path: '/',
name: 'app',
exact: true,
redirect: '/home'
}, ...childRoutes]
export default routes;
复制代码
用于读取模板文件,写入新的文件
首先第一行,告诉shell此文件默认执行环境为Node。
接下来咱们来看addFeatureItem(忽略个人命名╮(╯▽╰)╭),这个函数有三个参数:
咱们先确认文件是否存在,而后读取模板文件,写入新的文件便可,中间加了个已有文件判断。
是否是很简单!
最后加入使用commander建立本身的命令便可,更详细的用法能够查看commander的文档,这里添加一个简单的add命令,后跟一个featureName,键入命令后执行action函数,里面的参数即咱们刚刚键入的featureName,读取后即可以从模板建立新的feature。
固然,咱们还须要修改routeConfig.ts这个文件,我将这个操做放到了下面的ts-ast.ts文件。
#! /usr/bin/env node
const fse = require('fs-extra');
const path = require('path');
const _ = require('lodash/string')
const commander = require('commander')
const ast = require('./ts-ast')
const templatesDir = path.join(__dirname, 'templates');
const targetDir = path.join(__dirname, '..', 'src', 'features');
async function addFeatureItem(srcPath, targetPath, option) {
let res;
try {
await fse.ensureFile(srcPath)
res = await fse.readFile(srcPath, 'utf-8')
// avoid override
const exists = await fse.pathExists(targetPath)
if(exists) {
console.log(`${targetPath} is already added!`);
return
}
await fse.outputFile(targetPath, _.template(res)(option), {
encoding: "utf-8"
})
console.log(`Add ${srcPath} success!`);
} catch (err) {
console.error(err)
}
}
async function addFeature(name) {
const renderOpt = {
featureName: name,
featureUpperName: _.upperFirst(name)
}
const componentTmpl = `${templatesDir}/Component.tsx.tmpl`;
const component = `${targetDir}/${name}/${_.upperFirst(name)}.tsx`;
addFeatureItem(componentTmpl, component, renderOpt);
const indexTmpl = `${templatesDir}/index.ts.tmpl`;
const index = `${targetDir}/${name}/index.ts`;
addFeatureItem(indexTmpl, index, renderOpt);
const routeTmpl = `${templatesDir}/route.ts.tmpl`;
const route = `${targetDir}/${name}/route.ts`;
addFeatureItem(routeTmpl, route, renderOpt);
}
commander
.version(require('../package.json').version)
.command('add <feature>')
.action((featureName) => {
// add features
addFeature(featureName);
// manipulate some ts file like route
ast(featureName);
})
commander.parse(process.argv);
复制代码
用于修改rootConfig.ts文件
先给出ts-simple-ast的地址,本身仍是以为这个操做是比较复杂的,我也是参考了文档再加上项目脚手架代码才看明白,至于原理性的东西,可能还须要查看Typescript Compiler API,由于这个包也只是Wrapper,文档也还不是很完善,更复杂的需求还有待学习。
这里关键就两个操做,一个是添加一个import,其次则是修改childRoutes变量的值。可是一些函数的英文字面意思理解起来可能比上面的文件读写要困难。
const { Project } = require('ts-simple-ast')
const _ = require('lodash/string')
const path = require('path')
const project = new Project();
const srcDir = path.join(__dirname, '..', 'src', 'common');
async function addRoute(featureName) {
const FeatureName = _.upperFirst(featureName);
try {
const srcFile = project.addExistingSourceFile(`${srcDir}/routeConfig.ts`);
srcFile.addImportDeclaration({
defaultImport: `${FeatureName}Route`,
moduleSpecifier: `../features/${featureName}/route`
})
const routeVar = srcFile.getVariableStatementOrThrow('childRoutes');
let newRoutes = [];
routeVar.getDeclarations().forEach((decl, i) => {
decl.getInitializer().forEachChild(node => {
newRoutes.push(node.getText());
})
decl.setInitializer(`[${newRoutes.join(', ')}, ${FeatureName}Route]`);
})
await project.save();
console.log("Add route successful");
} catch(err) {
console.log(err);
}
}
module.exports = addRoute;
复制代码
只要头部加入了#! /usr/bin/env node
,简单的一行命令便可搞定chmod +x /filePath/yourExeFile
而后,咱们即可以使用/filePath/yourExeFile add featureName
的方式添加一个新的页面!