最新动态
- 泛微Ecology 9代码块开发框架:内置Web服务器
问题 通过Web访问本地构建出来的脚本文件,是我们这套开发框架的核心组成部分之一。目前是通过外置Web服务器nginx来实现的。 新开发人员加入团队时,除了要拉取和启动代码库本身,还要去安装、配置、启动nginx,比较麻烦,也容易出错,因为大家对nginx都不怎么熟悉。 因此,希望将Web服务器内置到开发框架中,只需要启动开发框架这一处就能开始开发,就像Create React App框架的npm run start命令那样。 技术选型 使用基于Node.js的后端框架Express实现内置Web服务器。 实现 安装依赖express。 本地Web访问只在开发时需要,因此只在持续构建模式下运行内置Web服务器。 在持续构建模式下,创建Express应用,响应dist/中的静态文件,然后监听8000端口。 // Express应用 const app = express(); // 响应dist/中的静态文件 app.use(express.static(DIST_DIR_PATH)); // 监听8000端口 app.listen(8000, () => console.log("Web服务器已启动")); 注意:要先停掉nginx服务器,并移除nginx中的配置,避免端口号冲突。
- 泛微Ecology 9代码块开发框架:编译JSX和SCSS
编译JSX 问题 OA系统前端使用React开发,我们的业务代码中也会写React组件。React组件通常用JSX语法编写(形如<Component/>),然后编译成原生JS语法(形如React.createElement(Component, ...))。我们的开发框架目前对JS代码只是简单地组装,浏览器运行的就是我们直接写出来的代码,因此在JS源文件中只能写原生JS语法,这对编写React组件很不方便。因此,开发框架需要支持JSX语法的编译。 技术选型 使用Babel实现将JSX语法编译为原生JS语法。 兼容性 JSX语法是原生JS语法的超集,IDE支持在.js扩展名的源文件中使用JSX语法,因此,我们直接将已有的index.js统一看作JSX语法,统一将其编译为原生JS语法。 实现 安装依赖@babel/core、@babel/preset-react。 修改构建程序: const babel = require("@babel/core"); /** 编译JSX语法为原生JS语法 */ function compileJSX(code) { const OPTIONS = { presets: ["@babel/preset-react"], }; return babel.transformSync(code, OPTIONS).code; } /** 获取JS代码 */ function getJS(scriptDirPath) { const jsxFilePath = path.resolve(scriptDirPath, "index.js"); const jsx = fs.readFileSync(jsxFilePath, {encoding: "UTF-8"}); return compileJSX(jsx); } // 构建时 const js = getJS(scriptDirPath); 编译SCSS 问题 用原生CSS语法写复杂样式时很麻烦,因此我们希望通过CSS预处理器(CSS preprocesssor)来降低复杂性。 技术选型 使用Sass的SCSS语法,它是原生CSS语法的超集,增加了嵌套、变量、单行注释等方便的特性,易学易用易读。 使用sass库将SCSS语法编译为原生CSS语法。 兼容性 之前已经写了不少index.css,但这里不能像JS那样,直接将.css的内容看作SCSS语法,因为IDE不支持,会报错。 我们有两种解决办法: 将所有index.css全部改名为index.scss,然后开发框架只支持index.scss,并要求团队以后必须使用index.scss这个名称,这样更简单。 同时支持index.css(原生CSS语法)和index.scss(SCSS语法),并规定index.scss优先于index.css,这样两种名称都可以用。 考虑到团队的实际情况,考虑到所有人都熟悉CSS但并非所有人都熟悉SCSS,考虑到SCSS只在少数复杂场景下使用才有必要,那么,应该保留对index.css的支持。 实现 安装依赖sass。 修改构建程序: const sass = require("sass"); /** 编译SCSS语法为原生CSS语法 */ function compileSCSS(code) { return sass.compileString(code).css; } /** 获取CSS代码 */ function getCSS(scriptDirPath) { const scssFilePath = path.resolve(scriptDirPath, "index.scss"); const cssFilePath = path.resolve(scriptDirPath, "index.css"); if (fs.existsSync(scssFilePath)) { const scss = fs.readFileSync(scssFilePath, {encoding: "UTF-8"}); return compileSCSS(scss); } else if (fs.existsSync(cssFilePath)) { return fs.readFileSync(cssFilePath, {encoding: "UTF-8"}); } else { return ""; } } // 构建时 const css = getCSS(scriptDirPath); 异常处理 之前的构建只是简单地读取、组装,所以一直没有遇到构建失败的情况。但在支持JSX和SCSS的编译后,构建就不一定能成功了,比如SCSS代码中有语法错误,编译时就会抛异常,导致整个构建程序退出。 因此,要在构建脚本目录时做异常处理,在控制台中报告构建失败,并打印异常。
- 泛微Ecology 9代码块开发框架:持续构建
问题 上一篇文章我们实现了基本构建功能,做到了在业务代码开发完成之后构建为脚本文件,但是在开发过程中还是不方便,因为每次修改都要重新手动运行构建。因此,我们需要实现持续构建,即每当源文件发生修改时,就自动重新构建所在的脚本目录。 初步实现 在构建程序中,创建全局变量watched,存放已监听的源文件路径列表,用于防止重复监听。 每当构建一个脚本目录时,除了构建脚本文件,还对源文件做监听。对于每个涉及的源文件(index.js、index.css(如果有);不监听template.js,因为不变),如果不在watched中,则: 调用fs.watchFile(),注册回调函数,监听源文件的改变。当源文件内容改变时(前后的mtimeMs不一致),则重新构建源文件所在的脚本目录(不递归)。 将源文件加入watched。 启动构建程序时: 删除dist/。 对src/执行构建(递归)。 在对src/递归构建一遍后,此时所有源文件都已被监听,每当源文件修改都会自动重新构建。 构建模式 构建有两种场景: 开发时,需要持续构建。 部署时,需要一次性构建。 因此,我们让构建程序接收一个可选的命令行参数--watch,决定是否对源文件监听,即是否开启持续构建。 将package.json改为: "scripts": { "build": "node index.js", "watch": "node index.js --watch" } 那么,可通过如下命令执行不同的构建模式: # 一次性构建模式 npm run build # 持续构建模式 npm run watch 优化监听性能 实测发现,通过fs.watchFile()实现监听,在发生修改到执行回调之间,有很大的延迟,这导致修改代码后总是不能立即重新构建,刷新页面时加载的还是旧代码,要等一会儿刷新,新代码才生效。 查阅Node.js API文档发现,fs.watchFile()是通过轮询文件的状态信息(stats)来检测文件变化的,效率很低。文档推荐使用fs.watch(),因为它是直接通过操作系统机制(通知、事件等)实现的,效率要高得多。 因此,将fs.watchFile()改为fs.watch(),将判断文件内容变化的条件改为eventType === "change"。 实测,及时性有了显著提高,可以做到立即重新构建。 支持源文件增删 目前只实现了对已有源文件修改的监听,但实际开发过程中还有增加、删除源文件的场景,例如: 新建一个脚本目录,新建index.js,开始开发新功能。 编写代码时发现需要写样式,因此新建index.css。 编写代码时发现不需要样式了,因此删除index.css。 目前只能重新启动构建程序,否则增、删源文件不会触发重新构建。 查阅fs.watch()的文档发现,它支持递归监听整个目录,目录下任何层级的文件变化都会触发回调,通过回调的filename参数可以知道是哪个文件/目录发生了变化。这就类似于在window上集中监听DOM树中所有节点的事件。 那么,现在不需要watched变量了,只需要在src/上注册一次递归监听。当src/中的任何文件/目录发生变化时,不必区分变化类型(增、删、改),直接将变化文件所属的目录作为脚本目录,对其调用构建函数(非递归)。在构建函数中,如果脚本目录存在,且index.js存在,则构建,否则不构建,即可。
- 泛微Ecology 9代码块开发框架:初步实现
目标 开发框架的初版仅实现以下最基本的特性: 版本管理。 规范化目录结构。 基本构建功能:将CSS和JS源文件合并在一起构建为成品JS文件。 可通过Web访问本地的成品JS文件。 技术选型 Git:实现版本管理。 JavaScript语言:开发框架使用JS编写,与业务代码的编程语言保持一致。 Node.js:开发框架运行在Node.js环境上,因为涉及文件操作。 NPM:管理三方库的依赖。 nginx:实现Web服务器(实现初版开发框架时,我还没想到内置Web服务器)。 实现 1. 版本管理 建立Git仓库。 业务上各个功能独立演进,因此不做版本号管理。 2. 规范化目录结构 package.json(NPM项目配置文件) index.js(构建程序,是开发框架的核心) template.js(构建所需的模板JS代码文件) src/(业务代码目录) 流程布局/(存放用于流程布局的代码块的业务代码) 流程名称/(存放指定流程的业务代码) 功能名称/(存放指定功能的业务代码,通常功能名称中说明适用于哪个/哪些流程节点) index.js(业务代码的JS源文件,必须有,一个目录中有index.js,就说明要将该目录构建为成品JS文件) index.css(业务代码的CSS源文件,可有可无) 建模布局/(存放用于建模布局的代码块的业务代码) 模块名称/(存放指定模块的业务代码) 功能名称/ index.js index.css 建模查询/(存放用于建模查询的代码块的业务代码) 模块名称/(存放指定模块的业务代码。建模查询通常隶属于建模模块,且一个功能通常在同属于一个模块的多个查询中复用,因此通常按模块组织目录。) 功能名称/ index.js index.css dist/(成品JS文件输出目录,src/A/B/C/(有index.js)将会构建为dist/A/B/C.js。Git忽略此目录。) 在src/中,流程布局/、建模布局/、建模查询/这3个子目录是固定的,因为OA系统中只有这3个地方可以写代码块。但它们下面的目录结构是不固定的,上述结构只是一个参考,具体要按实际情况合理划分。 为了方便叙述,我们做如下定义: 脚本文件:dist/中的成品JS文件。由脚本目录构建得到,最终会作为一个独立的脚本部署到OA系统中。注意与JS文件区分。 脚本目录:src/中任意层级的包含index.js的目录。有index.js就说明要将该目录构建为一个脚本文件。 JS源文件:脚本目录中的JS源文件。注意与脚本文件区分。 CSS源文件:脚本目录中的CSS源文件。 3. 基本构建功能 模板 template.js:封装jQuery.ready回调(OA系统给的规范)、作用域隔离(通过IIFE(立即调用函数表达式)实现)、设置CSS等基础代码,并预留CSS和JS的占位符。代码如下: jQuery(document).ready(function () { // 设置CSS (function () { let css = `/*[[CSS]]*/`; css = css.trim(); if (css) { let style = document.createElement("style"); style.innerHTML = css; document.head.append(style); } })(); // 执行JS (function () { /*[[JS]]*/ })(); }); 构建程序 index.js(构建程序): 删除dist/。 递归遍历src/中的每个目录,如果目录中有index.js,则为脚本目录,需要构建。 读取index.js,得到JS代码。 如果index.css存在,则读取之,得到CSS代码,否则CSS代码为空。 读取template.js,得到模板代码。 用JS代码、CSS代码分别替换模板代码中的JS占位符、CSS占位符,得到脚本文件代码。 根据src/A/B/C/→dist/A/B/C.js的对应规则,得到脚本文件的路径。 将脚本文件代码输出为脚本文件。 启动命令 在package.json中配置: "scripts": { "build": "node index.js" } 那么,在编写完业务代码后,就可以运行如下命令将整个src/构建为dist/: npm run build 4. 通过Web访问 使用nginx做静态Web服务器,配置如下: server { listen 8000; server_name localhost; root "<项目目录>/dist"; charset utf-8; } 启动nginx后,即可通过形如http://localhost:8000/流程布局/流程名称/功能名称.js的URL访问脚本文件。
- 泛微Ecology 9代码块开发框架
背景 公司使用泛微的Ecology 9系统作为OA系统,以满足公司内部的流程审批、业务数据存储与使用等内部管理需求。 代码块,是OA系统提供的自定义Web前端代码的功能,可以在流程布局、建模布局、建模查询页面中编写自定义的HTML代码(在HTML中又可以通过<style>和<script>编写CSS和JS代码),实现自定义功能。即,这是一种二次开发机制。 原始开发模式 刚起步时,我们的开发模式非常原始: 在OA后台中,打开代码块编辑界面(需多次点击)。 编辑代码块,编写CSS和JS代码(一般不写HTML,而是在JS中动态控制DOM)。 保存代码块。 打开/刷新代码块所在的页面。 运行代码,查看效果。 因为代码总是要多次修改才能写好,所以上述整个过程都要不断循环。开发过程图示如下: 缺点: 除了编写代码本身,还必须做很多步骤的额外操作,非常繁琐,开发效率很低。 没有版本管理,容易改乱。 CSS和JS写在一起,关注点未分离,心智负担较重。 多个功能写在一起,功能复杂时,心智负担很重。 代码复用只能靠手动复制,同步修改困难。 同一个页面只有一个代码块,多个开发人员不能同时开发,因为保存时会互相覆盖。 OA的在线代码编辑器只有有限的代码提示,不如IDE好用、高效。 可见,在这种原始开发模式下,我们很难有效地完成开发任务。 本质上,这些问题跟在Web古早时期直接手写一个个HTML文件是一样的,Web后来发展出了前端工程,通过前端开发框架解决种种开发时的问题。受此启发,我设计出了如下开发框架。 开发框架 目标 解决开发模式的问题,简化代码的开发、部署、管理过程,提高开发效率。 思路 建立Git仓库做版本管理,将开发框架和业务代码都放入其中。 规范化代码组织结构。 支持分开编写CSS和JS源文件,然后合并在一起构建为成品JS文件。 支持Sass、JSX等高级语法的编译。 支持持续构建,源文件修改后,自动重新构建。 支持将复杂的功能拆分为多个源文件。 支持源文件之间代码复用。 支持多个开发人员在同一个页面上各自开发不同的功能。 支持开发环境、生产环境的环境配置。 内置Web服务器,提供开发时的成品JS文件访问。 增强IDE的代码提示,支持提示OA系统API。 将OA系统API二次封装成公共库,屏蔽细节、增强功能、简化使用,提高开发效率。 业务代码的开发过程 启动开发框架,框架会在本地运行起Web服务器,并持续构建代码。 在开发环境的OA系统的代码块中不直接编写CSS和JS代码,而是通过<script src="..."></script>引用本地的成品JS文件。 在开发框架内编写代码(可用IDE),每次保存都会自动重新构建成品JS代码。 打开/刷新代码块所在的页面。 运行代码,查看效果。 改进后的开发过程图示如下: 可见,循环缩短了,繁琐的OA后台操作变成了一次性工作,循环中只剩下了开发本身最必不可少的编写和运行操作。 业务代码的部署过程 将成品JS文件上传至生产环境的OA服务器,然后在OA系统中将<script>的src改为服务器上的地址即可。 实践成果 实践表明,此开发框架显著提高了开发效率。通过不断解决实际遇到的问题,开发框架已经能够支持复杂、大型的业务需求的开发。