前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >前端工程化在WMS 6.0中的实践

前端工程化在WMS 6.0中的实践

作者头像
京东技术
发布2022-08-25 18:44:24
9370
发布2022-08-25 18:44:24
举报
文章被收录于专栏:京东技术京东技术

Tech 导读 在对大型前端项目进行国际化改造时,经常会遇到过工作量大、干扰项多以及容易遗漏等问题。而针对这些大量的重复的工作,自动化工具往往能提升很大的工作效率。本文将带读者了解node cli开发的基础知识,并对如何开发一个国际化校验工具来解决这些问题展开教学。

01 

背景

在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了!

仓储中台的愿景是,以用户为根本,通过发现、定义、设计、交付可被多BP复用的WMS能力,建设以仓储中台为主导的前中台协同研发内部共生态,帮助BP低成本地快速满足WMS相关业务诉求。wms6.0 依据此愿景进行建设,旨在提供轻量部署、灵活配置、高度产品化的仓储管理系统。

为了更好的支撑业务发展,提升用户体验,降低用户接入成本,wms6.0 各个子系统于年初开始相继进行国际化改造。web端基于vue开发,于是决定使用与之配套的『Vue I18n』作为解决方案。

02

  遇到的困难 

理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕

前端工程国际化改造的预估工期较长,主要原因是改造面临以下问题:

1.工作量大

  • 6.0前端工程包含9个子工程,其中8个工程确认必须国际化,单个子工程文件量大
  • 由于前期业务的快速迭代,未考虑国际化,国际化需要从零开始,代码改造量大

2.干扰项多

  • 代码中中文注释的存在,会对有效中文的检索定位造成干扰
  • 有些文件包含中文但是不需要国际化,也会对中文检索造成干扰

3.容易遗漏

  • 在改造完成后,传统的方案是人工检查,这很容易遗漏一些场景,导致校验不够充分
  • 接手其他人的工作,代码逻辑不够熟悉

因此,通过工具来提高生产效率和校验的准确度变得尤为重要!

03

  解决方案 

理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。

基于以上问题,在搜索相关资料并对比多个工具的实现方法后,决定使用『node cli』作为工具的实现方案,通过AST来精准识别有效中文和i18n方法调用。

使用『node cli』作为工具的实现方案,有以下几个原因:

  • 『node cli』使用 javascript 进行开发
  • 对于vue和js文件的解析有很好的第三方库支持支持
  • windows 和 macos 跨平台使用
  • IDE(WebStorm、VS Code)无关

整体的实现思路分为以下几步:

1.通过设定好的搜索规则,找到符合要求的vue和js文件,支持忽略指定目录或文件

2.读取文件的内容,将js文件转换为JS-AST,vue文件转换为Template-AST

3.通过相应的方法对AST进行遍历,在找到符合条件的代码片段后,对改造结果进行校验,并记录校验结果

4.通过文件路径合并校验结果并输出到文件中

04 具体实现

4.1 依赖库介绍

4.1.1 glob

node的glob模块使用 *等符号, 来写一个glob规则,像在shell里一样,获取匹配对应规则的文件,本次需要使用glob的sync方法进行同步搜索。

glob.sync(pattern, [options])

  • pattern {String} 待匹配的模式
  • options {Object}
  • return: {Array} 匹配模式的文件名

4.1.2 fs

fs包含node提供的一系列文档操作api,本次用到的是fs同步文件读取方法 readFileSync。

4.1.3 babel提供的工具库

@babel/parser是 babel 的核心工具之一,提供两种解析代码的方法:

  • babelParser.parse(code, [options]):解析生成的代码含有完整的 AST 节点,包含File和Program层级。
  • babelParser.parseExpression(code, [options]):解析单个 js 语句,该方法生成的 AST 不完整,所以使用@babel/traverse必须提供scope属性,限定 AST 节点遍历的范围。

@babel/traverse 提供遍历JS-AST节点的方法

@babel/types 用于判断节点类型

目前主流 JS 编译器例如 @babel/parser 定义的 AST 节点都是根据 estree/estree: The ESTree Spec (github.com) 规范来的,可以在 AST explorer 在线演示。

4.1.4 @vue/compiler-sfc

vue单文件组件(SFC)内部模板语法得到的 AST 和 JS 的AST区别很大,需要使用 @vue/compiler-sfc 来解析单文件组件,compiler-sfc 解析后的内容只需要关注 template 和 script 里的内容即可。

4.1.5 esbuild

esbuild一个JavaScript Bundler 打包和压缩工具,它可以将 JavaScript 和TypeScript代码打包分发在网页上运行,「/build/index.js」使用该工具构建。

4.2 初始化项目

4.2.1 创建项目

代码语言:javascript
复制
mkdir wms-i18n-checkcd wms-i18n-checknpm init -y

4.2.2 创建可执行文件

在 『wms-i18n-check』 根目录下新建一个文件『bin/index.js』

代码语言:javascript
复制
#!/usr/bin/env node'use strict';require('../build/index.js');

在 『package.json』 中添加配置项,然后在『/build/index.js』 实现 cli 能力

代码语言:javascript
复制
{  "bin": {    "wms-i18n-check": "./bin/index.js"  }}

4.3 核心实现

4.3.1 整体流程

图1 vue单文件组件解析流程

js文件的解析包含在了vue文件的解析逻辑中,所以这里以vue文件的处理过程为例。

主要的流程如上图所示:

1.使用 @vue/complier-sfc 将vue SFC 转换为Template-AST

2.分别对解析结果中的 template 和 script 进行处理:

  • template 是解析<template>标签部分得到的AST,其内部节点主要分为两种类型 props 和 children。
  • children内部需要处理两种类型的子节点,type为5代表节点使用了插值语法(INTERPOLATION),拿到内部代码后,按照标准js代码处理即可;type为1代表节点为元素(ELEMENT),需要继续作为   Template-AST进行递归处理。
  • 遍历props,找到 type 为7(DIRECTIVE)的节点后,按照标准js代码处理即可。
  • script是解析<script>标签内部JS得到的标准js代码,需要使用 @babel/parser 将其转换为JS-AST,然后使用@bable/traverse进行节点遍历。

3.将单个文件的校验结果合并后写入到 checkResult.json 文件中

4.3.2 核心代码

1.识别 vue 和 js 文件进行不同的逻辑处理

代码语言:javascript
复制
// parse.tsimport { parse as vueParser } from "@vue/compiler-sfc";import { parse as babelParser } from "@babel/parser";
export function parseVue(code: string) {  return vueParser(code).descriptor;}
export function parseJS(code: string) {  return babelParser(code, {    sourceType: "module",    plugins: ["jsx"],  });}
代码语言:javascript
复制
valid() {    if (!Object.values(FileType).includes(this.fileType)) {      logError(`Unsupported file type: ${this.filename}`);      return;    }
    if (this.hasI18NCall(this.sourceCode)) {      if (this.fileType === FileType.JS) { // js文件        this.collectRecordFromJs(this.sourceCode)      } else if (this.fileType === FileType.VUE) { // vue文件        const descriptor = parseVue(this.sourceCode);        if ( // <template>部分ast          descriptor?.template?.content &&            this.hasI18NCall(descriptor?.template?.content)        ) {          this.collectRecordFromTemplate(descriptor?.template.ast)        }
        if ( // <script>部分ast          descriptor?.script?.content &&          this.hasI18NCall(descriptor?.script?.content)        ) {          this.collectRecordFromJs(descriptor.script.content)        }      }    }  }

2.遍历JS-AST

通过遍历 CallExpression 类型的节点就能覆盖所有的 i18n 方法调用,对于类似 i18n.t(status === 1 ? 'a', 'b') 这种条件表达式的国际化方法调用,需要拿到前后两个 i18n key:consequent 和 alternate。

代码语言:javascript
复制
collectRecordFromJs(code: string) {    const ast = parseJS(code);
    const visitor: Visitor = {      CallExpression: (path) => {        const source = path.toString()        if(this.onlyHasI18NCall(source)){          const node = path.node          const args = node.arguments          const i18nNode = args[0]          if(i18nNode.type === 'ConditionalExpression') {            const consequentKey = ((i18nNode as ConditionalExpression).consequent as StringLiteral).value            const alternateKey = ((i18nNode as ConditionalExpression).alternate as StringLiteral).value
            try {              const consequentLang = getLang(consequentKey)              const alternateLang = getLang(alternateKey)
              this.records.push({                keys: {                  consequentKey,                  alternateKey                },                source,                result: {                  consequentLang,                  alternateLang                },                valid: consequentLang !== consequentKey && alternateLang !== alternateKey              })            } catch (e) {              this.records.push({                keys: {                  consequentKey,                  alternateKey                },                source,                valid: false,                errorMsg: (e as PropertyResolverError).message              })            }
          } else {            const i18nKey = (i18nNode as StringLiteral).value            try {              const lang = getLang(i18nKey)              this.records.push({                i18nKey,                source,                result: lang,                valid: lang !== i18nKey              })            } catch (e) {              this.records.push({                i18nKey,                source,                valid: false,                errorMsg: (e as PropertyResolverError).message              })            }          }        }      }    };
    babelTraverse(ast, visitor);  }

3.遍历Template-AST

使用 @vue/complier-sfc 将 vue 组件文件转换为 Template-AST,然后分别解析。

代码语言:javascript
复制
collectRecordFromTemplate = (ast: ElementNode) => {    /**     * v-pre 的元素的属性及其子元素的属性和插值语法都不需要解析,     */    if (        ast.type === 1 &&        /^<+?[^>]+\s+(v-pre)[^>]*>+?[\s\S]*<+?\/[\s\S]*>+?$/gm.test(            ast.loc.source        )    ) {      return    }
    if (ast.props.length) {      ast.props.forEach((prop) => {        // vue指令        if (            prop.type === 7 &&            this.hasI18NCall((prop.exp as SimpleExpressionNode)?.content)        ) {          this.collectRecordFromJs((prop.exp as SimpleExpressionNode)?.content)        }      });    }
    if (ast.children.length) {      ast.children.forEach((child) => {
        // 插值语法,如果匹配到 getLang()字符,则进行JS表达式解析并替换        if (            child.type === 5 &&            this.hasI18NCall((child.content as SimpleExpressionNode)?.content)        ) {          this.collectRecordFromJs((child.content as SimpleExpressionNode)?.content )        }
        // 元素        if (child.type === 1) {          this.collectRecordFromTemplate(child);        }      });    }  };

4.遍历js、vue文件进行解析

代码语言:javascript
复制
glob.sync(options.pattern!, { ignore: options.ignore })    .forEach((filename) => {        const filePath = path.resolve(process.cwd(), filename);        logInfo(`detecting file: ${filePath}`);        const sourceCode = fs.readFileSync(filePath, "utf8");        try {            const { records } = new Validator({code: sourceCode, filename, getLangCheck: options.getLangCheck});            if(options.onlyCollectError) {                const errorRecords = records.filter(item => !item.valid)                if(errorRecords.length > 0) {                    locales[filePath] =errorRecords                }            } else {                locales[filePath] = records            }        } catch (err) {            console.log(err);        }    });

05 成果

通过以上步骤就可以实现一个国际化校验工具了。在使用工具时,通过简单的配置即可检索指定项目指定路径下所有的 vue 和 js 文件,并且支持按文件路径来记录校验的结果并输出到 json 文件中。使用此工具可以有效降低校验的时间成本,同时工具提供的能力还能帮助使用人员快速定位问题代码,快速修复问题。

得益于工具提供的能力,整个项目的国际化耗时降低35%左右。在后续开发的过程中,可以使用该工具持续降低开发时间成本,提升校验的准确率,还能有效覆盖到历史代码,防止改动对现有逻辑造成影响。现在该工具已推广到wms其他前端工程中进行使用,反响还不错。

工具开发之初,为了快速投入到生产中,目前只支持vue和js文件的解析,暂时未对ts、tsx和jsx文件的解析进行支持,后续会根据需要提供相应的能力。

推荐阅读

可视化服务编排在金融APP中的实践

水滴低代码搭建——6倍提效,新品首发素材审核系统实践之路

京东科技埋点数据治理和平台建设实践

基于SPI的增强式插件框架设计

求分享

求点赞

求在看

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-08-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 京东技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 04 具体实现
  • 05 成果
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档