大家好,我是赤兔前端工作室的前端架构师,下面准备给大家分享一个公司项目的前端架构设计,以下所要分享的内容就是关于我如何来实现对业务系统的支撑的,不过内容不会很复杂,适合各阶段同学观看,如何大家有什么想法可以留意分享,一起讨论。
我会从以下几个点来详细介绍:
1.原有的项目设计的问题
2.如何优化架构
3.如何实现插件
原有的项目设计的问题
这也是目前前端的通病,主要是针对中小公司的,对于这部分公司来说,互联网资源有限,所以在开发质量这块是参齐不齐的,举个例子,A公司在项目起步时,前端起手就是一个vue create xxx
或者create-react-app
,然后不断的下一步下一步,全家桶一装,一个前端项目就搭建好了,当然这个开发方式没有任何问题,毕竟无论是VUE-cli
还是create-react-app
都是官方编写能适用于大部分项目的开发工具。是经过大量团体验证的,但是在我所接手的这个项目恐怕就不适用了。
我接手的项目原有代码就是用create-react-app
所搭建的(用vue
的同学先不要着急,因为后面的内容不针对框架类型),一能正常运营业务的项目,没有任何问题,可是当结合到业务类型后,问题就出来了。
图一:
参考图一,最下面的业务系统就是原有的项目,其实就是一个管理系统,使用create-react-app
创建,所以项目结构就没什么好说的了,大家随便调用下create-react-app xxx
就能看到一模一样的结构,起初时项目只是在公司内部使用,但随着业务发展,比如客户公司B需要购买或者入驻到我们的业务系统后,那么该系统就不能单独只服务一个公司,甚至除了B以外还会出现C或者D公司,有点SAAS
的味道了
在我接手之前已经发展到两个公司的服务,作为两个独立的公司在业务方面肯定有所差异,对一些功能模块需求也不一样,所以需要在项目内部对不同的公司进行适配,当前实现方案是通过域名和用户信息进行判断,这种方式只能说勉强维持,如果C,D公司再接入进来,那结果只能是变得没法维护。
其实到这里大家应该都想到解决方法了,给每个客户都创建一个单独的项目不就完了,然后公用的部分就抽离组件,确实,这就是一个很完美很通用的方案,但是实现起来可没那么简单,稍有不慎就会出现需要同时改动多个项目的情况,毕竟业务相关的组件是要跟随业务进行变化的。
如何优化架构
根据上面的分析我们总结了两点,一:需要一个应用框架对项目进行统一管理,二: 对项目的依赖统一管理,比如第三方包的版本之类的。所以有以下的应用框架的设计
图二:
框架名字暂且叫ebuild吧,下面简单解释下每个模块的作用,
core
core里面主要是包含一些必要的第三方包,列表如下:
react // 不解释
react-dom // 不解释
react-router-dom // 不解释
mobx // 类redux,vuex
mobx-react // 类react-react
classnames // 样式处理
复制代码
这样是为了统一第三方包版本管理,并且项目在依赖时无需每个包都单独安装一遍,只需要npm install @ebuild/core
即可,实现简单方便又实用。
除了上面上面的第三方包以外,还有一个很重要的createApp
模块,没有设计这个模块之前我们的入口文件代码是这样的
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(
<App />,
document.getElementById('root')
)
复制代码
很传统的起手式,之所以要对这部分进行处理,主要是想引入一个app
的概念,我希望每一个单页应用有一个全局的app
,能贡献关联上下文,下面来看看实现了createApp
的写法
import { createApp } from '@xsbuild/core'
import routes from './router'
const app = createApp({
el: '#root'
})
app.setRouter(routes)
app.render(props => {
return <app.rootView {...props}/>
})
复制代码
有同学是不是觉得这代码很像Vue
, 但其实里面差别很大的,稍后我会解释,不过可以提前说下,这是一个支持“多页的应用框架”。
createApp
部分代码
class Application extends ApplicationRender {
constructor(options) {
super(options);
}
setMeta(data) {}
removeMeta() {}
_setMetaByLocal(data) {}
setRouter(routes) {}
setSubRouter(routes) {}
setStore(store) {}
setRedirectWith401(callback) {}
render(view) {
if(typeof view === "function") {
super.render(view);
}
}
}
function createApp(opts) {
return new Application({
...opts
});
}
export default createApp;
复制代码
CLI
CLI主要用来管理项目的,主要命令有
- ebuild project
- ebuild app
- ebuild create:plugin
- ebuild use:plugin
这些命令类似vue create
或者create-react-app
用直接生成项目的,只是每个命令对应的功能不一样,实现的方案采用的是yeoman。
project实现代码:
const Generator = require("yeoman-generator");
const path = require("path")
class ProjectGenerator extends Generator {
constructor(args, opts) {
super(args, opts);
this._setupGenerator()
}
_setupGenerator() {
this.argument("name", {
type: String,
required: false,
description: "项目名称"
})
this.option("description", {
type: String,
description: "项目描述",
});
}
async setOptions() {
this.projectInfo = {};
["name", "description"].forEach(v => {
if (this.options[v]) {
this.projectInfo[v] = this.options[v]
}
});
}
promptProjectName() {
const prompts = [
{
type: "input",
name: "name",
message: "项目名称:",
when: this.options.name == null,
default: "xsbuild-start"
},
{
type: "input",
name: "description",
message: "项目描述:",
when: this.options.description == null,
default: this.options.name || '',
},
];
return this.prompt(prompts).then(props => {
Object.assign(this.projectInfo, props);
});
}
scaffold() {
this.destinationRoot(this.projectInfo.name);
this.fs.copyTpl(
path.resolve(__dirname, '../templates/project/**'),
this.destinationPath(""),
{
project: this.projectInfo,
},
{},
{
processDestinationPath: destPath => destPath,
globOptions: {
dot: true,
nobrace: true,
noext: true,
},
},
)
this.fs.move(
this.destinationPath(`package.json.ejs`),
this.destinationPath(`package.json`),
);
this.fs.move(
this.destinationPath(`.gitignore.ejs`),
this.destinationPath(`.gitignore`),
);
}
async end() {
this.log();
this.log(`${this.projectInfo.name} 项目创建成功.`);
this.log();
this.log("下一步:");
this.log();
this.log("$ cd " + this.projectInfo.name);
this.log(`npm install or yarn install`);
this.log(`npm run start`);
this.log();
}
}
module.exports = ProjectGenerator
复制代码
CLI-SERVER
CLI-SERVER的实现要稍微复杂点,主要因为是webpack和vite之间的我最后选择了vite,首先说说为什么选择vite,其实就一个字“快”,vite速度是webpack+babel的10-100倍,具体的解释同学们可以参考vite官方文档,这里不过多介绍了,可是快归快但是缺点也明显,用的人暂时相对来说还少那么坑就不会少,而且解决方案也少,简单来说webpack的插件都是拿来即用,但是我用vite在实现这个框架时就写了4个插件。
回到CLI-SERVER,它的功能主要是用于管理开发,构建,测试,部署的脚本的,这里我分别实现了dev
,build
,test
,deploy
,并且读取项目中一个自定义的配置文件作为依赖, 比如名为ebuild.config.js
的配置文件,下面是对配置项的一些解释
module.exports = {
// 全部页面的head配置
appGlobal: {
title: "--title--",
links: [],
scripts: [],
meta: [{name: "description", value: "--description--"}],
},
//存放apps的开发目录,常见的是src
appRoot: "pages"
// 多页app配置,里面的每一项都是一个单页app
apps: {
index: {
title: "--index--",
},
},
// 插件
plugins: {},
// 原有的vite配置项
viteConfig(source) {
return {
...source
};
},
};
复制代码
其他
注意helper这些就不过多解释了,就是存放一些常用的工具函数而已。
如何实现插件
统一的应用框架有了,但是一开始我们讨论的问题似乎还没解决,那就是组件的问题,应用框架只是方便我们管理多个项目,但是里面一些具体的业务组件那又是另外一回事了,这里我们再根据实际出发来找找解决办法。
既然我们项目要适配多个客户,有了多页的设计后能不能把一个大的功能模块给拿出来做成一个独立页面呢,在需要的时候再安装到项目中,颗粒化,可拆卸,可配置的,所以就有了下面的插件设计思路。
图三:
插件单独开发,开发完毕后发布到npm仓库,哪个项目要使用就直接安装,修改配置即可使用,效果如下:
图四:
这样layout作为一个独立的组件,其他功能模块抽离成插件进行开发,以上就是整套架构的实现思路了。
最后项目代码地址:github.com/hsian/ebuil…