起因
最近刚刚完成了一个前端To C的项目,想着把搭建的框架抽成一个脚手架的template,说起脚手架,不得不提的就是业界的老大哥yeoman,还有就是最近几年比较火的create-react-app、vue-cli、angular-cli,相对与专业度而言,后面三款是针对于单一的框架来写的,集成度比较高,社区经验比较丰富。
技术栈
先说一下我们的技术栈,我们使用的nextjs + koa。
nextjs处理React组件的SSR渲染,而koa和koa-middlewares则负责用户状态管理和接口代理。
因为我们用的是react的技术栈,针对业务上比较多的通用功能,我们也希望能够通过组件模板的方式,对组件进行动态的生成。
举个栗子,在我们开发的过程中,经常会有某些页面要判断用户是否已经登录,或者参数不正确的情况,此时我们的通用处理是跳转到主页。
因为是SSR的项目,在服务端的时候我们已经可以知道用户是否登录了,此时我们可以直接调用nextjs的钩子函数getServerSideProps,一段伪代码如下:
export async function getServerSideProps({req:http.IncomingMessage,res:http.ServerResponse} ) {
if (!userLogin) {
res.writeHead(302, {
location: '/home'
});
res.end();
}
return {
props: {},
}
}
如上代码我们都可以通过代码生成工具来一键生成,目前我能想到有如下方案。
- 自定义js模板字符串,通过js对象动态传入值,搭配
node的fs接口渲染文件 - snippets
常规的Simple React Snippets 可以通过简单的字母组合来生成一段react代码。
我对项目的结构进行了优化,将原写在pages目录下的代码拆分为两部分,分别放到pages和views目录下。
pages目录下的组件进行路由的控制,以及服务端数据的抓取views目录下存放着组件和以及样式文件
这样做的好处是,如果后续SSR想换成CSR,重写pages目录下代码即可。
简单来看的话我们的目录是下面这个样子的
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ └── vercel.svg
├── src
│ ├── pages
│ │ ├── _app.js
│ │ ├── home.js
│ │ ├── index.js
│ └── views
└── styles
├── Home.module.css
└── globals.css
如果我们想创建一个订单查询页面,那我要同时去创建如下两个个文件src/pages/order/index.js,src/views/order/index.js。
站在工程化的角度,我们可以使用工具一键创建这些代码,snippets的问题是需要先去创建一个文件,然后在文件中去生成,此处我们要创建的文件比较多,可以使用一个小工具plop, 简单来讲,plop的工作原来是通过终端交互的方式获取参数,根据事先定义好的模板和获取到的参数进行结合,生成代码到指定的目录下。
下面我将一步步来搭建一个使用plop的nextjs项目。
使用如下命令来初始化一个nextjs项目
先简单调整一下代码结构,使用如下命令
mkdir src && mv pages src
mkdir -p src/views
调整_app.js和index.js中styles文件夹的相对路径。
index.js
- import styles from '../styles/Home.module.css'
+ import styles from '../../styles/Home.module.css'
_app.js
- import '../styles/globals.css'
+ import '../../styles/globals.css'
新建一个测试页面
mkdir -p src/pages/my && touch src/pages/my/setting.js
mkdir -p src/views/my && touch src/views/my/setting.js
src/pages/my/setting.js代码如下
import SettingView from '../../views/my/setting'
const SettingPage = () => {
return <>
<SettingView />
</>
}
export default SettingPage
export async function getServerSideProps({ req, res }) {
const login = true;
if (!login) {
res.writeHead(302, { location: '/' })
res.end();
}
return {
props: {}, // will be passed to the page component as props
}
}
src/views/my/setting.js 代码如下
const SettingView = () => {
return <div>SettingView</div>
}
export default SettingView
站在业务的角度,如果我想对自己的账号进行设置,则需要先进行登录,此处直接通过更改变量来测试,不引入接口。
启动项目
npm run dev
浏览器打开链接http://localhost:3000/my/setting,此处我们模拟的是用户已经登录情况,可以看到浏览器已经显示了
更改src/views/my/setting.js中代码
- const login = true;
+ const login = false;
刷新页面,可以看到浏览器跳转到了http://localhost:3000,看一下网络
关于此处的逻辑,简单来想是可以抽象成一套模板的,假定后面我有一些其他的逻辑,比如订单查看,密码修改,逻辑是类似的。
我们引入plop小工具
npm i plop -D
touch plopfile.js
mkdir -p plop-templates/login-controller
touch plop-templates/login-controller/view.hbs
touch plop-templates/login-controller/page.hbs
修改package.json
{
"script": {
// other scripts
"plop": "plop"
}
}
如下是抽取出来的模板代码
view.hbs
const {{firstUpperCase name}}View = () => {
return <div>{{firstUpperCase name}}View</div>
}
export default {{firstUpperCase name}}View
page.hbs
import {{firstUpperCase name}}View from '../../views/{{model}}/{{name}}'
const {{firstUpperCase name}}Page = () => {
return <>
<{{firstUpperCase name}}View />
</>
}
export default {{firstUpperCase name}}Page
export async function getServerSideProps({ req, res }) {
const login = false;
if (!login) {
res.writeHead(302, { location: '/' })
res.end();
}
return {
props: {}, // will be passed to the page component as props
}
}
如下为plopfile.js的代码
const modelNames = ['my', 'order'];
module.exports = async function (
/** @type {import('plop').NodePlopAPI} */
plop
) {
plop.setHelper('firstUpperCase', function (text) {
return text && text.replace(/^\S/, s => s.toUpperCase());
});
plop.setGenerator('login-controller', {
description: '新增带有登录逻辑判断的代码',
prompts: [
{
type: 'list',
name: 'model',
message: '请选择模块名',
choices: modelNames.map((item, index) => ({
key: index.toString(),
name: item,
value: item
})),
}, {
type: 'input',
name: 'name',
message: '请输入组件名称'
}
],
actions: [
{
type: 'add',
path: 'src/pages/{{model}}/{{name}}.js',
templateFile: 'plop-templates/login-controller/page.hbs'
},
{
type: 'add',
path: 'src/views/{{model}}/{{name}}.js',
templateFile: 'plop-templates/login-controller/view.hbs'
}
]
});
};
简单介绍一下这段代码
/** @type {import('plop').NodePlopAPI} */是为了引入typescript的声明文件,方便我们编写配置plop.setHelper是handlebars的转换工具函数,作用是将首字母大写setGenerator设置生成器,如果有多个的话,在执行的时候会提示去选择prompts是使用问答的方式让开发同学来填写信息type:list是通过单选的方式来让开发同学选择type:input是通过输入的方式来让开发同学选择actions是与终端交互后执行的动作,我们目前有两个动作,是同时根据模板在指定目录下新增了两个文件
看一下终端的执行效果
此处只是起到抛砖引玉的效果,有兴趣的小伙伴可以去官网看一下详细的教程。
demo所在路径为https://github.com/huomarvin/next-plop-demo,感兴趣的同学,可以去查看。