- 基于 GitLab CI 的 小程序自动化部署
小程序发布了 miniprogram-ci 已经一段时间了。 不建议采用通过命令行来触发上传动作, 通过 miniprogram-ci 更为方便 小程序中的 体验版 和 开发板 开发版: 开发者通过 [代码]微信开发者工具[代码] 上传代码的版本。不同开发者上传后,对应着不同的 [代码]开发版[代码],[代码]开发版[代码] 不能直接被访问到。通过 微信开发者工具 上的 [代码]预览[代码] 功能,生成二维码预览(二维码有效时间较短 20~30 min)。 体验版: 在 微信公众平台 后台,指定某个 [代码]开发版[代码] 指定为 [代码]体验版[代码]。[代码]体验版[代码] 长期有效,只能指定一个。 所以如果我们可以将 [代码]体验版[代码] 始终指定某一个开发者 的 [代码]开发版[代码] 上,然后每次通过这个开发者上传小程序代码。这样就只需要访问 [代码]体验版[代码] 即可。 小程序自动部署 常用的 CI/CD 有很多,由于小程序上传代码只能通过 [代码]微信开发者工具[代码]。 而 [代码]微信开发者工具[代码] 只能支持 Windows/Mac 而不支持 Linux,所以最后决定采用 GitLab CI。 因为 [代码]GitLab CI[代码] 中的 GitLab Runner 可以执行 GitLab CI 中的任务。我们使用 [代码]GitLab Runner[代码] 在自己电脑上注册个 Runner 来执行上传小程序代码的任务。 GitLab CI 配置流程 下载 GitLab Runner Install,注册一个 Runner 运行 gitlab-runner register 根据命令提示操作 [代码]# 输入 GitLab 服务地址 Please enter the gitlab-ci coordinator URL (e.g. <https://gitlab.com/>): https://gitlab.com # token 在对应项目中的 Setting => CI/CD => Runners 中 如下 图1 Please enter the gitlab-ci token for this runner: hVGe8TvU5KL-ZQqsv48z # 输入描述信息 Please enter the gitlab-ci description for this runner: this is description for the runner # 注意: 这里输入的 tags 会在后面 .gitlab-ci.yml 配置的时候用到。 Please enter the gitlab-ci tags for this runner (comma separated): test # 是否作执行没有指定 tags 的 builds Whether to run untagged builds [true/false]: false # 是将 Runner 锁定至当前项目 Whether to lock the Runner to current project [true/false]: true # 注册成功 Registering runner... succeeded runner=hVGe8TvU # 输入 Runner 执行环境 Please enter the executor: docker, docker-ssh, shell, parallels, ssh, virtualbox, docker+machine, docker-ssh+machine, kubernetes: shell Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded! [代码] [图片] 这个时候就注册号好了。我们去到项目的 Setting => CI/CD => Runners 下会发这个 Runner [图片] 注意: 此时这个 Runner 前面是个 感叹号,我们运行 [代码]gitlab-runner start [代码] 配置 .gitlab.yml 文件,在项目根目录下创建 .gitlab-ci.yml 文件 [代码]image: node:9 # 制定任务执行时的 docker 镜像 stages: # 定义阶段用于执行任务(定义了 build 和 deploy 两个阶段) - build - deploy build: # 定义 build 任务 stage: build # 该任务属于 build 阶段 script: # 执行下面脚本 - yarn install - yarn build artifacts: # 制定任务成功后保存的文件(可以在 GitLab 上下载) name: $CI_JOB_NAME untracked: false # 是否添加所有 git untracked 的文件 paths: # 需要添加文件的位置 - dist tags: # tags 制定运行在哪个 Runner 上 - gitlab-org deploy: # 定义 deploy 任务 stage: deploy # 该任务属于 deploy 阶段 dependencies: # 该任务依赖 build 阶段的所有任务 - build variables: # 定义变量 VERSION: 1.0.0 DESCRIPTION: 描述内容 CLI: C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat script: # 执行下面脚本 # 下面是 小程序 上传代码的脚本,以 Windows 为例 # https://developers.weixin.qq.com/miniprogram/dev/devtools/cli.html#4-%E5%91%BD%E4%BB%A4%E8%A1%8C%E4%B8%8A%E4%BC%A0%E4%BB%A3%E7%A0%81 - %CLI% -u %VERSION%@%cd% --upload-desc %DESCRIPTION% tags: # 这里需要在我们刚注册的 Runner 运行,和注册时的 tags 匹配 - test [代码] 注意:小程序上传代码通过开发工具的 命令行调用 GitLab Pipelines 当我们提交将代码 push GitLab 后, 当 GitLab 检测到 [代码].gitlab-ci.yml[代码] 文件后, 会自动触发 Pipeline 任务,如图: [图片] 我们在 GitLab 上看 这个 Pipeline 下的 Jobs 的运行状态 当 [代码]deploy[代码] Job 成功后,就表示我们自动化的上传代码流程成功。 修改开发者工具源代码。 由于在执行cli的时候,错误并不会抛出。(比如开发者工具没有登录,但是 cli 执行 upload 命令的时候错误没有抛出,导致就pipeline 显示成功) 所以我们需要在修改开发者工具目录下的 cli.bat 文件,添加一行内容 [代码]@echo off set CALLING_DIR=%CD% cd /d %~dp0 .\node.exe .\cli.js %* + if %errorlevel% neq 0 exit /b %errorlevel% ... [代码]
2021-05-28 - 聊聊如何给一个小程序历史老项目“减压”
前言 在日常的工作中,由于业务或者工作安排的需要,有时候需要我们参与到一些曾经没有接触过却 历史悠久 的项目当中,如果这个项目创建初期,创建者有很好的 前瞻性,并且严格遵循 code preview 等项目开发工作流,那代码看上去就会像是同一个人写出来一样,十分规范;否则,将会逐渐沦为一个 茅坑代码集合。 接下来我想分享一下近期对公司的一个小程序项目做的一些优化工作,会分别从以下几个方面进行阐述: 项目现状 项目拆解 搭建工具平台 项目地址 项目现状 1. 子模块分包不完全,存在子包内文件相互引用的情况 2. 个别没有用到的图片等静态资源文件没有及时删除,导致包体积过大,无法生成预览码 3. 测试同学反馈小程序测试流程过于繁琐,复杂,加大测试工作量和小程序的出错率 项目拆解 因此,需要针对以上提到的三个问题对这个项目进行初步的基于项目目录结构层面上的优化(不涉及到项目里面的业务代码,组件等冗余代码的优化) 其实也很容易理解,当你刚接手一个项目的时候,想必是先对这个项目的目录结构有一个总体初步的认识。 一、 “子模块分包不完全,存在子包内文件相互引用的情况” 1. 分析 如果是微信小程序项目,我们可以通过以下两种方法去快速了解一个项目的模块分包情况: 打开根目录下的 [代码]app.json[代码] 文件,找到 [代码]subPackages[代码] 字段,这就是当前项目的所有子模块数组集合;(缺点:人工肉眼查找,不智能) [代码]// app.json { "pages": [], "subPackages": [ { "root": "A", "pages": [ "pages/A-a", "pages/A-b", .... ] }, { "root": "B", "pages": [ "pages/B-a", "pages/B-b", .... ] } ] } [代码] 通过微信提供的 cli 命令行工具,查看当前的分包情况;(优点:不仅智能,还能查看每个子模块压缩后的包大小) [代码]cli preview --project f://workspace/rainbow-mp/xinyu [代码] 效果如下: [图片] 通过cli工具分析出来的结果,我们可以很明显看出当前项目总共分了哪几个子模块,以及这些子模块经过微信压缩工具(实则微信开发者工具编译)之后的大小,由此得出的当前项目存在的问题有如下几点: 主包([代码]main[代码])体积已经超过了微信规定的 [代码]2MB[代码]最大值,无法生成预览码用于移动端测试(问题很严重); 子包分包不合理,将子包的子目录作为分包的入口(如:[代码]/daojia/pages[代码], [代码]/deptstore/pages[代码], [代码]/index/pages/userMaterial[代码], [代码]/shopping/pages[代码]),而不是将子包根目录本身作为拆包的入口,导致其余目录下的文件统一打包到了主包中,造成主包体积变大; 2. 细分拆包 换句话说,如果我们把子包的分包不合理的问题给解决了,主包([代码]main[代码])的体积过大的问题自然而然也就解决了。 定位到问题就相当于解决了一半。 接下来就是想办法把子包根目录更改为模块打包的入口,这时候有人会说了,把 [代码]app.json[代码]文件下的[代码]subPackages[代码]模块数组字段的每个模块的[代码]root[代码]的值都改成子模块的根目录不就完事了吗? 没毛病,做法就是这样 但是,在修改之前得保证拆出来子包根目录下的其余子目录下的文件并没有被别的模块引用,否则就会出现文件引用错误的bug。 因此,大致有以下两种做法可以参考一下,我采用的是第一种: 对当前拆解子包外的其余模块(包括主包[代码]main[代码]) 进行全文件扫描,通过正则的方式过滤出 [代码]require[代码]引用到的文件路径,进而分析是否有子包下的文件被别的模块引用; 复写 [代码]require[代码] 方法(因为我们项目中文件引入的方式是[代码]require[代码]方式); 这里简单说明一下我不采用第二种方法的原因: [代码]require[代码]方法没有挂载在[代码]global[代码]全局下(因为接下来我需要写脚本在node环境下运行),因此需要重写一个如[代码]myRequire[代码]的自定义函数,然后挂载到[代码]global[代码]对象下,接着全局匹配所有文件的[代码]require[代码]字符替换为[代码]myRequire[代码]; [代码]require[代码]是动态引入,也就是说,可以在[代码]js[代码]文件的任意处进行引入,写在了小程序的业务代码中,因为接下来的脚本文件是运行在微信开发者工具以外的环境,缺失了微信小程序需要的模块包,会导致编写的脚本分析文件报错; 3. 编写脚本 接下来就正式步入编码阶段了,其实思路比较简单,我大致从以下几点进行这次脚本文件的编写: 1. 获取当前项目的所有一级目录:除去当前需要拆解的子包以外的所有一级目录都需要进行全局文件扫描 [代码] * 获取当前路径下的第一层目录 * @param {*} path 项目路径 * @param {*} targetDir 子包的目录名 */ const getOwnDirectorys = async(path, targetDir) => { const dir = await fs.promises.opendir(path) const result = [] for await (const dirent of dir) { const isDir = await isDirectory(`${path}/${dirent.name}`) // 也就是说,除去子包以外的目录都需要进行全局文件扫描 if (isDir && dirent.name !== targetDir) { result.push(`${path}/${dirent.name}`) } } return result } [代码] 2.过滤出每个一级目录下所有js和json文件 读取到目录了,那接下来自然就是遍历这些一级目录,然后获取到这些目录下的所有资源文件,那为什么只是过滤其中的[代码]js[代码]和[代码]json[代码]文件出来呢? 经过一段时间的接触之后,我发现: 子包的组件存在被别的模块包引用的情况,而小程序的组件引入主要是通过[代码]json[代码]文件的[代码]usingComponents[代码]字段; 子包的[代码]js[代码]文件也存在被别的模块包引用的情况,多数发生在一些工具函数,接口调用文件上; 因此,为了减少扫描文件的数量和提高效率,先针对项目中每个模块的[代码]js[代码]和[代码]json[代码]文件进行扫描匹配。 [代码]const filterJsAndJsonFiles = async (dirItem, filterDirs) => { const subDir = await fs.promises.opendir(dirItem) const jsFiles = [] const jsonFiles = [] for await (const dirent of subDir) { // 不需要分析的目录直接跳过 if (!filterDirs.includes(dirent.name)) continue const currentFiles = getAllFiles(`${PROJECT_NAME}${dirItem}/${dirent.name}`) // 过滤若干不同类型的文件数组 currentFiles.forEach(fileItem => { const extname = path.extname(fileItem) if (extname === '.json') { jsonFiles.push(fileItem) } if (extname === '.js') { jsFiles.push(fileItem) } }) } return { jsFiles, jsonFiles, } } [代码] 3. 文件查找 & 匹配 到这里,我们已经拿到了每个模块对应下的所有[代码]js[代码],[代码]json[代码]文件,接下来就需要针对这些文件进行分析了,大致思路分为以下两点: [代码]json[代码]文件分析:读取文件内容,将[代码]json[代码]字符串转为[代码]json[代码]对象格式,过滤出[代码]usingComponents[代码]字段,查找匹配出拆解子包的组件; [代码]{ "usingComponents": { "a": "./A/a", "b": "../B/b", "c": "../C/c" } } [代码] [代码]js[代码]文件分析:读取文件内容,通过[代码]正则表达式[代码]过滤出[代码]require[代码]引入的文件字符数组,从中查找匹配出拆解子包内的文件引用; [代码]const a = require('../../a.js') const b = require('./b.js') .... [代码] 脚本编写: json文件组件引入分析: [代码]/** * 统计json文件引入到的组件数组 * @param {*} jsonFile */ const listComponents = (jsonFile) => { if (!jsonFile) return const jsonDataStr = fs.readFileSync(jsonFile) const jsonData = JSON.parse(jsonDataStr) const componentList = [] if (jsonData) { const { usingComponents } = jsonData for (let key in usingComponents) { componentList.push({ name: key, path: usingComponents[key], filePath: jsonFile, }) } } return componentList } [代码] js文件[代码]require[代码]引入分析: [代码]const lineReg = /require\s*\([\'\"][\w\W]*[\'\"]\)/g // 子模块初始化 moduleResultMap[dirKey] = { componentImport: [], fileImport: {}, } jsFiles.forEach(filePath => { const fileContent = fs.readFileSync(filePath, 'utf8') // 为了避免无用查找,只针对前30行文本进行内容分析 const lines = fileContent.split(/\r?\n/).splice(0, 30) // 初始化子包目录文件名 moduleResultMap[dirKey]['fileImport'][filePath] = lines.reduce((acc, current) => { const matchArr = current.match(lineReg) return matchArr && matchArr.length > 0 && matchArr[0].indexOf('/daojia/') > -1 ? [...acc, matchArr[0]] : acc} , []) }) [代码] 4. 效果展示 最后,我是将分析出来的结果导出到了[代码]csv[代码]文件中,以便于为我接下来的拆包提供一份相对有保障的可视化的支持: [图片] 因为我这次主要是针对项目中[代码]daojia[代码]这个子模块进行一个拆包,因此分析的也是针对项目中其余子模块对该模块文件的一个引用情况做一个分析,表格中的每个字段所代表的意思我也大概说明一下: [代码]interface Table { module: string //子模块 type: string // 分析的文件类型 name: string // 分析的文件名 import: string // 引用的组件 || 引用的文件 filePath: string // 分析的文件路径 } [代码] 5. 终极展示 我们再回过头来看这幅图: [图片] 当我们成功地都将以下几个子包根目录从项目中剥离抽身之后,才会真的有底气地说:把[代码]app.json[代码]文件下的[代码]subPackages[代码]改下就好了 [代码]/daojia/pages -> /daojia /deptstore/pages -> /deptstore /index/pages/userMaterial -> /index /shopping/pages -> /shopping [代码] 再来看看现在的模块包分析表: [图片] 结论:经过合理化的分包之后,优化后的主包体积比优化前整整减少了35% 二、“个别没有用到的图片等静态资源文件没有及时删除,导致包体积过大,无法生成预览码” 1. 分析 在上一节里,我的做法概括起来:拆解子包,合理化模块打包 由于各种原因,一是在当前项目里面存在了过多的活动图片,重复的icon等等,但是当活动下架之后,这些图片并没有得到及时移除;二是组件引用混乱,相同组件的代码会同时出现在各个子模块里面; 这些无疑都是导致 项目体积过大 和造成 项目难以维护 的主要原因; 所以,在这一节里,也可以概括一句话:剔除无用资源,减少项目文件 2. 思路 总体来说,我也是通过写脚本来分析这些资源文件,思路如下: 无用图片资源查找 ① 根据不同模块配置信息,依次读取当前模块图片目录下的所有图片文件,过滤出图片文件名,存储在一个数组内; ② 然后全扫描这个项目内的所有文件,通过[代码]fs[代码]模块读取到文件的字符串内容,遍历图片数组,根据字符串匹配[代码]indexOf[代码],如果存在,则标记图片的引用路径;文件全扫描之后,如果找不到,则在路径一栏标记为“没有用到”; ③ 又或者匹配到的图片,则从数组内剔除出去,当扫描完所有的文件之后,剩下的就是没有引用到的图片文件了;(以上方法很蠢,但是胜在简单粗暴,希望有更好方法的朋友可以给我留言,不胜感激。) 组件引用分析 ① 根据不同模块的配置信息,依次读取当前模块内[代码]pages[代码]和[代码]components[代码]目录下的[代码]json[代码]文件(组件引入的入口),实则一个JSON字符串的转成JSON对象;[代码]JSON.parse(jsonstring)[代码] [代码]{ "usingComponents": { "a": "./A/a", "b": "../B/b", "c": "../C/c" } } [代码] ② 然后获取其相同文件名的[代码]js[代码]文件(页面或者组件的主体文件),通过[代码]fs[代码]模块读取文件内容,注意,这时候是得将这些富文本字符串转为DOM节点树结构对象,然后遍历节点对象,去匹配解析出对应的[代码]json[代码]组件引入入口文件下的json对象,然后分析出引用到的组件,实际就是节点标签名的匹配过程。 3. 脚本编写 这里就把一些核心代码贴出来就好,大家看看就好,不做过多阐述了 无用图片资源查找脚本 [代码] // 需要分析的图片目录地址 const imgDirPath = path.resolve(__dirname + '/../..' + imagesEntry); const imgFiles = getAllFiles(imgDirPath) if (imgFiles.length === 0) return // 只保留图片的文件名数组 const allImageFiles = imgFiles.map(imgItem => path.basename(imgItem)) // 查找所有的wxml, js文件 const allWxmlFiles = targetEntrys.reduce((acc, targetEntry) => { const targetDirPath = path.resolve(__dirname + '/../..' + targetEntry) const targetAllFiles = getAllFiles(targetDirPath, true) const allWxmlFiles = targetAllFiles.filter(filePath => { const extname = path.extname(filePath) return ['.wxml', '.js'].indexOf(extname) > -1 }) return [...acc, ...allWxmlFiles] }, []) // 遍历图片集数组,查找文件是否有引入 const result = allImageFiles.reduce((acc, imgName) => { const rowItems = allWxmlFiles.reduce((childAcc, filePath) => { const fileStr = fs.readFileSync(filePath, 'utf8') return fileStr.indexOf(imgName) === -1 ? childAcc : [...childAcc, { image: imgName, existPath: filePath, }] }, []) // 如果查找完毕数组为空,则说明没有引入到该图片 return rowItems.length === 0 ? [...acc, { image: imgName, existPath: '没有用到' }] : [...acc, ...rowItems] }, []) // 导出csv文件 const csv = new ObjectsToCsv(result) const exportPath = `${__dirname}${'/../..'}${BASE_EXPORT_IMG}/${imageReportFile}` await csv.toDisk(exportPath) [代码] 组件引用分析脚本 [代码] // 解析入口目录 const entryDir = path.resolve(__dirname + '/../..' + entry) const allFiles = getAllFiles(entryDir) if (allFiles.length === 0) return const filterFiles = getFilterFiles(allFiles, ['wxml', 'json']) // 组装导出对象数组数据 const pageWithComponents = filterFiles.reduce((acc, { jsonFile }) => { const current = path.basename(jsonFile, '.json') const currentDir = path.dirname(jsonFile) const components = listComponents(jsonFile) || [] if (components.length == 0) { return [...acc, { page: current, directory: currentDir, }] } else { // 输入wxml地址,转化为json标签对象 const fileJsonData = getFileJsonData(currentDir + `/${current}.wxml`) const childs = components.reduce((childAcc, { name, path: compPath }) => { let used if (fileJsonData) { used = isWxmlImportComponent(fileJsonData, name) used = used ? 'true' : 'false' } else { used = '解析出错' } return [...childAcc, { page: current, directory: path.resolve(currentDir), component: name, componentPath: compPath, used, }] }, []) return [...acc, ...childs] } }, []) // 导出csv文件 const csv = new ObjectsToCsv(pageWithComponents) const exportPath = `${__dirname}/../..${BASE_EXPORT_COMPONENT}/${exportFileName}` await csv.toDisk(exportPath) [代码] 结论:剔除没有引入的图片资源,减少项目体积;分析页面的组件引入,为项目的组件库的搭建提供数据支持。 三、“测试同学反馈小程序测试流程过于繁琐,复杂,加大测试工作量和小程序的出错率” 1. 分析 小程序测试步骤如下: 开发同学在功能提测阶段,需提供功能分支名给到测试同学,比如说:[代码]feature/monthcard[代码] 测试同学需要切换功能分支,并且拉取最新代码,执行 [代码]git checkout feature/montcard[代码] [代码]git pull origin feature/monthcard[代码] 打开 [代码]小程序开发者工具[代码],更改配置文件环境参数,如:[代码]config.js[代码],比如说修改成 [代码]env = test/dev/pre/pro[代码] 等等,切换到对应的接口环境进行测试 如只需要本地测试,直接在工具上面测试即可,如需要移动端测试,则需要点击 [代码]编译执行[代码] 生成小程序预览码,手机扫码测试 后期开发同学推了代码,需要同步测试同学定期去更新代码,执行: [代码]git pull origin 分支名[代码] 上面就是我司的关于小程序提测时的做法,相信这也是一部分公司的关于小程序的测试流程,又或者一部分公司的做法如下: 开发同学在本地生成测试预览码,然后将预览码截图发给测试同学进行测试(测试预览码有时效限制,需要开发每隔一段时间去重新生成一个新的预览码); 开发同学编写工具,将整个小程序代码包压缩放在内网的一个网页下,每次由测试下载到本地,解压,然后用开发者工具打开测试(一定程度自动化了测试流程和简化了测试同学的流程,但是依然很麻烦); 结论:总得来说,开发和测试同学都没有成功从上面的开发工作流中解耦出来。 2. 解决方案 基于上述的一些问题,我发现这一系列的测试步骤可以通过微信官方提供的ci命令行工具,是完全可以抽象出来,做成一个可以简化测试工作流的工具平台,听着是不是很棒? 下面就是我的一些调研发现: miniprogram-ci 是从微信开发者工具中抽离的关于小程序/小游戏项目代码的编译模块。 开发者可不打开小程序开发者工具,独立使用 miniprogram-ci 进行小程序代码的上传、预览等操作。 微信ci文档地址 搭建工具平台 前端(js) React 搭建前端骨架(借用facebook提供 [代码]create-react-app[代码] 脚手架即可) Bootstrap 作为前端界面布局的ui框架库 后端(nodejs) 采用 [代码]Express[代码] web应用开发框架搭建即可 安装 [代码]miniprogram-ci[代码] 包(构建预览码,提交发版等) 安装 [代码]html2json[代码], [代码]objects-to-csv[代码] 包(用于项目静态资源使用分析等) ps: 这里就不对里面的技术细节做过多阐述了,具体可以查看文末的项目地址,我已经开源出来了。 效果展示: [图片] 项目地址 出于代码保密考虑,开源项目里面分析的小程序源码采用的是自己的一个小程序项目作为分析的基础项目: 小程序项目脚本分析项目地址 https://github.com/csonchen/wx-mall-components 小程序项目的自动化构建服务平台 https://github.com/csonchen/mpcode-manage
2021-02-23 - border 1rpx不能正常显示
效果,如下图所示。 通过remote dug发现在在ios上,border:1rpx 被渲染为 0.5px。 ios12上部分border显示不正常,安卓ok。 [图片]
2018-12-21 - MapContext.includePoints 方法频繁调用缩放问题
- 当前 Bug 的表现(可附上截图) 按钮绑定方法,方法内部调用 MapContext.includePoints,当参数只有一个点的时候,每点击一次按钮会缩放一次地图,直至 scale 缩放到1为止 - 预期表现 不要缩放地图 - 复现路径 - 提供一个最简复现 Demo
2019-07-23 - Input组件在IOS上使用原生键盘选择展开选择文字时候失去焦点问题
已知问题,等待Input同层解决
2019-09-24 - input 搜索输入跳转下一页之后,回到原页面无法再次聚焦?
如题 ios 14.2 微信版本7.0.18 iphone8 不会有问题 升级微信版本 ios 14.2 微信版本7.0.20 iphone8 必现。
2020-12-30