有个朋友最近做了一下前端工程化的分享,笔者以前只是浅尝辄止,听完收获颇丰。考虑到工程化可以系统的完善自己前端方面的能力,所以笔者最近也开始做这个系列,从零开始,按基本结构,规范,工具,优化,自动化部署和性能监控的顺序,学习实践。正好我司让我们组开始做技术分享,笔者就分享了一下刚学的plop。分享,交流之后整理出来发上来记录一下。本文源码
一、plop概述
什么是plop?一个微型生成器框架,用于生成符合团队规范的模板文件。
可以简单的认为它是 inquirer对话框 和 hanldebar模版的简单融合。
生成模板文件?听起来好像和vsCode的用户代码片段差不多。如果只处理单文件的话,那他们确实差不多。plop受益于handlebars(中文文档)更强大的语义化模板功能,可能略强于代码片段。那么,仅仅如此吗?我们来试一下。
二、plop起步
-
安装
npm install --save-dev plop //npm install -g plop -
在项目根文件夹创建 plopfile.js
//plopfile.js module.exports = function (plop) {};它导出一个函数,这个函数接收一个 plop 对象作为第一个参数。
-
创建生成器generator
一个 plop 对象暴露一个包含
setGenerator(name, config)函数的 plop api。setGenerator用于创建一个生成器(generator)。当 plop 在当前文件夹或子文件夹的终端中运行时,这些generator将会以列表的形式被展示在终端中。下面是一个最简单的 generator :
module.exports = function (plop) { // 创建一个生成器 plop.setGenerator('component', { description: '新增一个公共组件',//描述,在终端里生成器后面显示的内容 prompts: [], // 提示,用于捕获用户输入 actions: [] // 行为,具体执行的内容 }); };prompts即是 inquirer的prompts,可参考
inquirer的文档,这里就不详细展开了。actions的标准属性如下:
属性 数据类型 默认值 描述 type String action 的类型(包括 add, modify, addMany force Boolean false强制执行(不同的 action 类型会对应不同的逻辑) data Object/Function {}指定 action 被执行时需要混入prompts答案的数据 abortOnFail Boolean true当一个 action 由于某种原因执行失败时停止执行之后的所有 action skip Function 一个用于指定当前 action是否需要被执行的函数 这些是plop最基本,也是最核心的内容。了解了这些之后我们就可以正式开始使用了,来吃苟!
三、plop初体验
下面以vue项目中新增一个vue文件作为示例来演示如何使用plop。因为后续使用的文件较多所以不再使用默认的plopfile.js。
-
在项目根目录新建一个文件夹 generators,文件夹中新增一个文件index.js
后续文件比较多,我们新增一个文件夹来存放相关文件
//generators/index.js module.exports = (plop) => { // 创建一个生成器 plop.setGenerator('component', { description: '新增一个公共组件',//描述,在终端里生成器后面显示的内容 prompts: [], // 提示,用于捕获用户输入 actions: [] // 行为,具体执行的内容 }); }; -
修改package.json
通过--plopfile命令来指定plop的入口文件地址为刚刚新建
//package.json { //... "scripts": { //... "generator": "plop --plopfile generators/index.js", }, } -
新建
hanldebar模板文件在generators文件夹下新建index.vue.hbs文件
<template> <div></div> </template> <script> export default { name: "{{ componentName }}", components: {}, props: { list: { type: Array, default: function() { return []; }, }, }, data() { return {} }, created() {}, mounted() {}, methods: {}, }; </script> <style scoped></style> -
完善生成器
//generators/index.js const componentExists = require('./utils/componentExists'); //componentExists是一个工具方法,用来验证是否已存在同名 module.exports = (plop) => { // 创建一个生成器 plop.setGenerator('component', { description: '新增一个组件', prompts: [ { type: 'input', name: 'componentName', message: '请输入组件名:', default: 'Button', validate: value => { if (/.+/.test(value)) { return componentExists(value) ? '已经存在相同的容器名或者组件名' : true; } return '组件名为必填'; }, }, ], actions: data => { const actions = [ { type: 'add', path: '../src/components/{{properCase componentName}}/index.vue', //componentName与prompts的name值对应,即为用户输入内容 templateFile: './index.vue.hbs', abortOnFail: true, }, ]; return actions; }, }); }; //utils/componentExists.js /** * componentExists * * 判断组件或页面是否存在 */ const fs = require("fs"); const path = require("path"); const pageComponents = fs.readdirSync( path.join(__dirname, "../../src/components") ); const pageContainers = fs.readdirSync( path.join(__dirname, "../../src/views") ); const components = pageComponents.concat(pageContainers); function componentExists(comp) { return components.indexOf(comp) >= 0; } module.exports = componentExists; -
实战
到这里我们就可以正式尝试一下了。
首先运行
npm run generator,再输入组件名称Test创建完成。再试一下组件名不输入、输入为空、重复输入,都没问题,这里就不放截图了占位置了。
那今天的分享到此为止......怎么可能!只有这些的话岂不是舍近求远,明明一个cv就能解决的事情。下面正式进行一下企业级的实战(吹牛 (= - =) )。
四、plop进阶
-
当我们新建页面时我们会做什么
一般来说,我们新增一个页面时会在views/pages文件夹下新建一个页面文件夹(当项目比较大时会分模块,再多一层文件夹),之后新建页面的vue文件,然后去配置路由,新增对应的vuex模块,引入封装的api等等...
那现在来尝试用plop做一下这些事情。
先调整一下之前的文件结构,在generators文件夹下新增component文件夹,把之前的生成器配置和模板文件放进去。再新增view文件夹用来放后续的文件。generators/index.js文件调整如下:
const componentGenerator = require('./component/index.js'); const viewGenerator = require('./view/index.js'); module.exports = (plop) => { plop.setGenerator('component', componentGenerator); plop.setGenerator('view', viewGenerator); };在view文件夹下新增index.vue.hbs文件同上文同名文件一样。
view/index.js文件调整如下:
const componentExists = require('../utils/componentExists'); module.exports = { description: '新增一个视图容器(页面)', prompts: [ { type: 'input', name: 'viewName', message: '请输入容器(页面)名:', default: 'Form', validate: (value) => { if (/.+/.test(value)) { return componentExists(value) ? '已经存在相同的容器名或者组件名' : true; } return '容器名必填'; }, }, ], actions: (data) => { let actions = [ { type: 'add', path: '../src/views/{{ viewName }}/index.vue', templateFile: './view/index.vue.hbs', abortOnFail: true, }, ]; return actions; }, };目前为止view和component这两个生成器已经功能相同了。
-
新建页面时的路由处理
src目录下新建router文件夹:
//router/index.js import Vue from 'vue'; import Router from 'vue-router'; import Allrouters from './routers.js'; Vue.use(Router); export default new Router({ mode: 'hash', routes: [ // 默认首页 { path: '*', redirect: '/index' }, ...Allrouters, ], }); //router/routers.js //-- append import here -- export default [ //-- append router here -- ];注意//-- append import here --和//-- append router here --和下文action及模板文件里的正则验证保持完全一致。
新建
router.js.hbs模板文件,新增两个modify类型action://view/router.js.hbs { path: '/{{ viewName }}', name: '{{ viewName }}', component: {{ viewName }}, }, //-- append router here -- //view/index.js ... actions: (data) => { let actions = [ { type: 'add', path: '../src/views/{{ viewName }}/index.vue', templateFile: './view/index.vue.hbs', abortOnFail: true, }, { type: 'modify', path: '../src/router/routers.js', pattern: /(\/\/-- append import here --)/gi, template: "const {{ viewName }} = () => import('../views/{{ viewName }}/index.vue');\n$1", }, { type: 'modify', path: '../src/router/routers.js', pattern: /(\/\/-- append router here --)/gi, templateFile: './view/router.js.hbs', }, ]; return actions; }, ...两个
modify类型action会修改routers.js文件,实现同步路由。这里默认的是页面不分模块的情况,假如分模块的话,新加一个输入模块名的prompt,修改相应逻辑,多加一层就可以了。如果路由也分模块的话,可以配合webpack的require.context来实现,根据项目实际情况动态调整。 -
配置vuex
vuex我参考了一个朋友修改的结构和规范,不是一定要这样,用自己最熟悉的那套最好。查看代码在view文件夹下新增与上图一一对应的模板文件。查看模板
增加一个
confirm类型的prompt,与相关模板的action://view/index.js prompts: [ //... { type: 'confirm', name: 'vuex', default: true, message: '是否使用vuex?', }, ], actions: (data) => { let actions = [ //... ]; if (data.vuex) { let store = [ { type: 'add', path: '../src/views/{{ viewName }}/store/constants.js', templateFile: './view/constants.js.hbs', abortOnFail: true, }, { type: 'add', path: '../src/views/{{ viewName }}/store/actions.js', templateFile: './view/actions.js.hbs', abortOnFail: true, }, { type: 'add', path: '../src/views/{{ viewName }}/store/getters.js', templateFile: './view/getters.js.hbs', abortOnFail: true, }, { type: 'add', path: '../src/views/{{ viewName }}/store/mutations.js', templateFile: './view/mutations.js.hbs', abortOnFail: true, }, { type: 'add', path: '../src/views/{{ viewName }}/store/index.js', templateFile: './view/index.js.hbs', abortOnFail: true, }, ]; actions = actions.concat(store); } return actions; },模板文件能过获取到prompts中提供的参数,配合handlebars自身的模板语法,实现根据用户选择动态控制模板,修改页面模板文件
index.vue.hbs如下://view/index.vue.hbs <template> <div></div> </template> <script> {{#if vuex}} import { STORE_NAME, GET_TOTAL_ACTION, SET_TOTAL_MUTATION } from './store/constants'; import storeOption from './store/index'; import { mapState, mapActions, mapMutations } from 'vuex'; {{/if}} export default { name: "{{ viewName }}", components: {}, props: { list: { type: Array, default: function() { return []; }, }, }, data() { return {}; }, computed: { {{#if vuex}} ...mapState({ total: (state) => state[STORE_NAME].total, }), {{/if}} }, {{#if vuex}} beforeCreate(){ this.$store.registerModule(STORE_NAME,storeOption)//动态注册vuex模块 }, {{/if}} created() {}, mounted() {}, methods: { {{#if vuex}} ...mapActions({ getToalAction: `${STORE_NAME}/${GET_TOTAL_ACTION}` }), ...mapMutations({ setToalMutation: `${STORE_NAME}/${SET_TOTAL_MUTATION}`, }), {{/if}} }, {{#if vuex}} destroyed(){ this.$store.unregisterModule(STORE_NAME,storeOption)//离开页面时要卸载vuex模块,否则再次进入页面vuex的action会多次触发 }, {{/if}} }; </script> <style scoped></style> -
就这?
到这里好像感觉也就...嗯,还行?
仔细想想我们修改的路由文件很可能是个公用文件,其他同事也可能会在里面新增修改,为了避免冲突,最好新建页面之后立即推送到git。咦,node可以执行脚本命令,那可不可以把git操作也一并处理了?尝试一下。
导入exec,自定义polp的action:
//generator/index.js const { execSync } = require('child_process'); //node的子进程, execSync是exec的同步版,衍生shell 并在该 shell 中运行命令 const componentGenerator = require('./component/index.js'); const viewGenerator = require('./view/index.js'); module.exports = (plop) => { plop.setGenerator('component', componentGenerator); plop.setGenerator('view', viewGenerator); plop.setActionType('git', (answers, config) => { try { execSync(`git pull`); execSync(`git add .`); execSync(`git commit -m "新建${config.file}"`); execSync(`git push`);//这些只是示例,不代表真实情况 } catch (err) { throw err; } }); }; //view/index.js actions.push({ type: 'git', viewName: data.viewName, });setActionType方法可以自定义actions(类似于
add或modify)。默认参数如下:参数 类型 描述 answers Object generator prompts的答案 config ActionConfig generator action的配置对象 plop PlopfileApi 用于action所在的plop文件的plop api 由此可见,plop或者说node可以做任何我们想让他做的事情。
-
其他
前面几步实现了基础的路由,vuex配置,git提交。但是真实项目中肯定还有更多的通用配置要加入进去,比如我们项目的页面都有统一的基础布局,那模板又丰富了:
//view/index.vue.hbs <template> <div class="pagesDiv"> <page-little-menu :title="[ { name: '数据治理' }, { name: '问题设备查看', path: {{ viewName }} }, ]" /> <div class="tableDivForNoCondition"> </div> </div> </template>再比如我们有动态计算el表格高度然后自适应的mixins,如果有表格的页面就可以选择性注入模板;
一般页面都是组件化开发,在页面文件夹下新建components文件夹,同时新增一个示例组件,然后在
index.vue中导入组件也是必不可少的;封装的
axios/fetch等api也是要复制进去的...等等太多了,这里就不多赘述了。我想每个老前端er都有新建,新建,新建,CV,CV,CV,CV然后才把页面基础结构搭好的经历。而使用plop之后只需要输入页面名称,选择一些配置,一个符合自身项目规范的页面结构就搭建完成了,想想就爽爆了。plop还有helper,partial等辅助功能,有兴趣朋友别犹豫了,你也开始吧。
五、总结
上文实现了从最基础的配置一个模板文件,到一个骨架丰富(简陋,但这里不想写了,后面的内容大同小异)的视图容器生成。相比它效率提升的作用,笔者其实更在意它对于规范提升的作用。使用它可以让整个项目保持整体的一致性,视图、数据、工具等拆分的越细,项目越像同一个人写的。这对于多人协作太重要了,在你修改或者接手同事的代码时,可以帮你快速适应代码,问题可能发生的地方或者你将要修改的地方,你都了如指掌。但当项目或者团队不够大,拆分的越细,写起来越麻烦,比如上文的vuex结构,可能每次新增,修改都要修改多个文件。而合并文件,取消常量设置,简化一下结构的话,可能plop效率提升作用更优于规范提升。
本文的视野只局限在页面级,但是放大看一下,把整个项目的的配置都写进来的话,再上传npm,那它就是个脚手架了?本系列完成之后试一试。
参考: