现状及问题
项目组前端比较少,为了快速开发,使用的是uniapp来发布多端项目,uniapp目前无法整合web,所以web端使用的是付费的iview-pro组件库来实现。
因为项目需求变动比较快,为了避免一套逻辑实现多次,使用的是嵌入的方式来复用web和uniapp相同的功能。
这会有如下几个问题:
- web端的SPA在初次加载时很慢,白页时间很长,用户体验不佳
- 通过自适应的方式适配手机和PC端,前端开发需要考虑两端,工作量较大,部分情况下,调整的难度可能并不比写两套页面简单。最终适配效果也不一定理想
- 嵌入页面与父页面的交互复杂,需要使用各种通信机制来处理通信,对前端要求较高,且不易于测试。流程上需要考虑多端,增加思考负担
- webview缓存问题,加载的页面可能会有缓存,导致不手动刷新无法清除缓存(部分情况即使刷新也无法清除缓存)
同时还有项目混乱的问题:
- uniapp和web端项目结构不统一
- 功能相同的组件有多套
- uniapp无法自动发布,需要基于HBuilderX打包后才能发布
目标
鉴于上面的问题,考虑在不增加前端开发负担的情况下,解决这些问题,期望:
- 一套代码在web和uniapp中公用,避免相同的功能在web和uniapp重复实现
- 只需要考虑单端流程,不需要考虑多端流程,降低前端思考负担(目前使用的嵌入方式是需要考虑多端流程和问题的,无形中增加了前端的思考负担)
- 统一web和uniapp的项目结构和语法,目前web使用npm-cli来管理,uniapp使用的hbuilder来创建的
- 统一部署方式(目前uniapp需要前端打包)
- 确定前端代码规范(目前前端代码较乱,发送请求的方式就有好几种!)
- 提供统一的用户体验(uniapp提供类原生体验,web提供网页体验),尽量避免嵌入导致的白页问题
思路
首先,uni-app在发布到H5时支持所有vue的语法;发布到App和小程序时,由于平台限制,无法实现全部vue语法。
具体差异详见:https://uniapp.dcloud.io/use
上文列出的差异数量有限,也就是说,只要避免了这些差异,就能保证uniapp和web代码的基本语法一致性,注意这里是基本语法。
其次,uniapp是支持vue-cli以及npm包依赖的。也就是说,uniapp和web可以共用仓库,这就解决了共用代码的管理问题。类似Java,将共用代码打包发布到仓库,uniapp和web分别引用的方式来使用。
具体见:
https://uniapp.dcloud.io/quickstart?id=_2-%e9%80%9a%e8%bf%87vue-cli%e5%91%bd%e4%bb%a4%e8%a1%8c
不过考虑到前期代码改动会比较频繁,以这种方式处理还是比较麻烦(需要修改、打包、发布、改版本号在install)。所以直接基于git的subtree来实现代码层面的共享(修改、提交、拉取),待代码稳定后,再进行库的管理。
最后,就是组件库问题。web目前使用的是iViewAdminUI,uniapp使用的是原生UI。
uniapp原生UI见:https://uniapp.dcloud.io/component/README
两者差异:https://ask.dcloud.net.cn/article/35489
两者的差异见上文,主要体现在:
- 兼容性:iView组件是针对浏览器的,里面有大量dom和window对象操作。但小程序和App是没有dom这些api的,所以无法跨端使用。即无法在uniapp里使用iView
- 性能:vue组件性能好于小程序自定义组件,即uniapp提供的组件性能要优于第三方组件性能
所以最主要的问题,就是如何在web端和uniapp端提供一套语法一致的UI组件库。再结合上面两个功能,即可提供一套多端语法完全一致的开发规范。
实际上,要提供一套语法一致的UI组件库并不难,只是需要花费一点时间,在原UI库上再封装一层即可。详见下节。
方案
计算机科学中遇到的所有问题都可通过增加一层抽象来解决。
All problems in computer science can be solved by another level of indirection。
by David Wheeler
简单来说,就是通过抽象一层来解决。
考虑到vue的使用人群更广,也更偏底层,所以在uniapp上抽象来适配vue。
这样能解决两个问题:
- 适配问题,由框架来处理,而不需要开发来考虑自适应问题。相当于将问题从运行期提前到了编译期。
- 不需要为了兼顾多端,而选择一个折中的方案。比如:为了手机端的展示,web端不能使用table。通过适配,可以在手机端将table适配成list。
假设,web端有如下代码:
{{item.name}}
这段代码在web端可以直接运行,但是Select在手机端其实并不友好,比较友好的方式应该是Picker,所以,我们就可以在uniapp里编写一个组件select.vue(这部分代码通过组件库的方式来编写,项目引用即可。)
当前选择:{{array[index]}}
将其注册到项目中,组件名叫Select,这样上面的Select就会被解释为Picker来处理了。
实施步骤
上面的方案,具体可以分为如下几步实施:
- 统一项目结构:迁移HBuilderX创建的项目到vue-cli。统一前端项目的结构和打包发布方式。
- 统一请求调用:封装统一的AJAX请求API供两端使用。初步构建common库,初步统一代码规范。
- 制定语法规范:只使用uniapp和vue均支持的语法,同时对于相同功能的代码,语法要保持一致。比如路由跳转。进一步统一规范。
- 二次封装uniapp组件:适配iView组件。封装统一的组件库,完成多端统一,这是一个持续过程。
迁移HBuilderX创建的项目到vue-cli
- 基于vue-cli创建uniapp
# 创建uni-app
vue create -p dcloudio/uni-preset-vue my-project
## 如果执行失败,可以访问如下git,下载zip包,解压
https://github.com/dcloudio/uni-preset-vue
## 然后执行
vue create -p #{解压目录} my-project
# 运行、发布uni-app
npm run dev:%PLATFORM%
npm run build:%PLATFORM%
%PLATFORM% 可取值如下:
- 将 HBuilderX 工程内的文件(除 unpackage、node_modules 目录)拷贝至 vue-cli 工程内 src 目录
- 在 vue-cli 工程内重新安装 npm 依赖(如果之前使用了 npm 依赖的话)
npm i node-sass -D
npm i sass-loader -D
基于git的subtree的代码公用方法
注意,虽然subtree可以任意修改提交,不过还是尽量在一处修改,比如在admin-vue项目中编写,否则合并时冲突解决比较麻烦。
使用步骤:
- 创建一个项目,用于存放需要公用的代码,正常创建即可(创建完后,基本就可以不用管了)
- 在需要使用同步代码的项目中执行如下命令(只需要执行一次):
# module是取的别名
git remote add -f module ${上面的项目git地址}
# 将这个项目拉取到 src/module目录下
git subtree add --prefix=src/module module master --squash
- 在项目中修改模块代码后,正常push。然后使用如下命令同步(每次修改共享代码后执行):
git subtree push --prefix=src/module module master
- 需要同步的项目,执行如下命令(如果需要同步共享代码,则执行):
git subtree pull --prefix=src/module module master
基于npm仓库的代码公用方法
下载npm包
npm config set registry http://***/repository/npm-public/
设置后,正常使用npm即可
上传npm包
通过
npm adduser
添加用户!
项目中需要有package.json,注意下面的配置
{
"name": "vue-tmp",
"version": "1.0.0",
"private": false,
"publishConfig" : {
"registry" : "http://***/repository/npm-releases/"
},
...
}
private为false才能上传,publishConfig配置的是上传的仓库路径
使用如下命令登录registry!
npm adduser --registry=http://***/repository/npm-releases/
在项目根目录下执行:
npm publish
即可上传成功
注意:scripts中不能有publish,否则会触发二次publish!正式releases仓库是关闭reploy的!
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"unit": "jest --config test/unit/jest.conf.js --coverage",
"e2e": "node test/e2e/runner.js",
"test": "npm run unit && npm run e2e",
"lint": "eslint --ext .js,.vue examples src test/unit test/e2e/specs",
"build": "node build/build.js"
},