前言
回顾近两年的代码,我们在移动端的开发大致经历的四个时期;我分别称之为:wepy时期、mpvue1.0时期、mpvue1.x时期、mpvue2.x时期
wepy时期
在这个时期,我们的开发业务主要有赞播集客小程序系列,项目主要有jike-weapp、jike-weapp-b、jike-h5、business-circle、share;当时的微信小程序还不像现在的如日中天,社区和生态还皆在萌芽,wepy当时推出的官方框架,确实解决了用原生小程序开发所面临的窘境。
wepy解决了什么问题?
- 支持使用三方npm资源,可以调用社区资源
- 开发风格,接近Vue.js,降低学习成本
- 支持组件化开发,便于复用,便于分工合作
- 自带编译器,模板,样式,es6等的支持
在项目中我用到的主要有哪些
打包工具:wepy自带的打包工具
请求工具:封装wepy的一套http工具类
全局混入工具: mixins
全局样式:stylus
环境配合: config
语法转义工具:babel
代码风格检查工具:eslint
其他:
wepy-async-function
wxapp-socket-io
wepy-com-toast
qqmap
wxchart
...
项目中遇到的主要问题
- 代码风格上,四不像,可以说是react和vue的杂交产品;在来回切换web端开发时,有较大的切换成本
- 标签只能使用小程序标签
- 数据渲染需要手动执行刷新操作
- 组件开发很多功能不支持,使用起来不顺手,比如数据传递,嵌套v-for循环组件不渲染的等问题
- 不支持集中数据管理
- 社区资源贫瘠,很多工具不能使用
- 在环境配置时,需要修改代码,有代码侵入的风险
mpvue1.0时期
在这个时间轴上,我们开发项目主要有IPC-Shopping、jike-shopping-mall、jike-merchant-mall、jike-business-work、jike-business-zhitui、jike-business-zhitui、jike-business-boss、jike-ws-client、jike-ws-radar、jike-agent-manager等等
可以看到,这个时期的项目很多,主要的运行环境是微信小程序和企业微信,所以面临着web端和移动端技术栈的反复切换。
随着美团的mpvue的横空出世,又给开发者们带来了一条新的开发思路与开发体验.其核心特点如下:
- mpvue 保留了 vue.runtime 核心方法,无缝继承了 Vue.js 的基础能力
- mpvue-template-compiler 提供了将 vue 的模板语法转换到小程序的 wxml 语法的能力
- 修改了 vue 的建构配置,使之构建出符合小程序项目结构的代码格式: json/wxml/wxss/js 文件
项目中用的到东西
- 移动端
打包工具:webpack
请求工具:基于flyio封装的一套http工具类
全局混入工具: mixins
全局样式:stylus
环境配合: config
语法转义工具:babel
代码风格检查工具:eslint
集中状态管理: vuex
其他:
alias配置,方便引用
mpvue-router-patch
mpvue-entry
wechat
svg-base64
we-paint
we-cropper
im
cos
...
- web端
打包工具:webpack
请求工具:基于axios封装的一套http工具类
全局混入工具: mixins
全局样式:stylus
环境配合: config
语法转义工具:babel
代码风格检查工具:eslint
路由:vue-router
集中状态管理: vuex
其他:
better-scroll
cropperjs
echarts
js-base64
vue-amap
vue-awesome-picker
vue-clipboard2
vue-cropperjs
vue-lazyload
weixin-js-sd
im
...
- 移动端(react-native)
打包工具:react-native
请求工具:fetch
全局样式:styls.js
路由: navigation
语法转义工具:babel
代码风格检查工具:eslint
集中状态管理: redux
其他:
react-native-splash-screen
react-native-swiper
react-navigation
react-native-camera
react-native-pull
react-native-qrcode-scanner
toggle-switch-rn
...
mpvue1.x时期
这个阶段的主要项目有zhidian-client,zhidian-radar,zhidian-business,zhidian-brand,mall-client,fresh-client,fresh-h5,purchase-client,sorting-client
经过项目的锤炼,我们对mpvue越发熟悉,对项目快速创建有的新的思考。pongni工具的诞生以及,hygen自动化创建页面模板给开发效率的提升,做出了重要的贡献
项目中用的到东西
- 移动端
自动创建页面等模板工具:hygen
打包工具:webpack
请求工具:基于flyio封装的一套http工具类
全局混入工具: mixins
全局样式:stylus
环境配合: config
语法转义工具:babel
代码风格检查工具:eslint
集中状态管理: vuex
其他:
alias配置,方便引用
base-64
wechat
qr-image
we-paint
we-cropper
im
cos
...
- web端
自动创建页面等模板工具:hygen
打包工具:webpack
请求工具:基于axios封装的一套http工具类
全局混入工具: mixins
全局样式:stylus
环境配合: config
语法转义工具:babel
代码风格检查工具:eslint
路由:vue-router
集中状态管理: vuex
其他:
better-scroll
cropperjs
echarts
js-base64
vue-amap
vue-awesome-picker
vue-clipboard2
vue-cropperjs
vue-lazyload
weixin-js-sd
im
...
mpvue2.x时期
这段时间的主要项目有fresh-client,exchange-client,exchange-business,exchange-h5,exchange-economy,LeagueMiniProgram,LeagueH5
面临的主要问题,随着项目不断的扩张,分包工具愈加迫切;这个阶段最大之一的贡献就是mpvue的升级以支持分包功能
随着用户流量的变革,多平台小程序需求日渐清晰,因此对mpvue的改造比不可少,目前支持了微信,百度,字节小程序,并且进行了初步排坑。
项目中用的到东西
- 移动端
自动创建页面等模板工具:hygen
打包工具:webpack
请求工具:基于flyio封装的一套http工具类
全局混入工具: mixins
全局样式:stylus
环境配合: config
语法转义工具:babel
代码风格检查工具:eslint
集中状态管理: vuex
其他:
alias配置,方便引用
qs
wechat
...
- web端
自动创建页面等模板工具:hygen
打包工具:vue-cli-service
请求工具:基于flyio封装的一套http工具类
全局混入工具: mixins
全局样式:stylus
环境配合: config
语法转义工具:babel
代码风格检查工具:eslint
路由:vue-router
集中状态管理: vuex
其他:
better-scroll
storage-controller
weixin-js-sdk
vue-meta
...
总结
框架最大的特点就是在于限制
目录结构剖析
- 版本一(v1)
src
api
api.js
base
component/component.wpy
common
image
xxx.png
js
http.js
libs
amap-wx.js
mixins
base.js
stylus
index.styl
icon
xxx.png
pages
home
home.wpy
utils
ald-stat.js
app.wpy
index.template.html
- 版本二(v2)
src
api
api.js
components
component/component.vue
design
index.styl
mixins
base.js
pages
main
home
modules
home.js
helpers.js
home.vue
config.js
state
module
index.js
helpers.js
store.js
utils
wechat.js
App.vue
app.json
main.js
- 问:你觉得v2和v1的最大区别是什么?
v2较v1相比,目录结构更加的扁平化;功能划分颗粒度更加细腻
- 问:你觉前端项目目录结构应该遵循一个怎样的规律?
- 文件夹和文件夹内部文件的语义一致性
例如api装接口相关的东西,component是组件,utils是工具...,这样的好处便于多人开发维护
- 尽可能的扁平化
扁平化简单的来说就是减少嵌套,好处就是方便查找,方便重构;缺点可能是弱化各页面之间的关系
- 单一入口、出口
我们的目录结构都会是一个文件夹按照路由模块(或者说是页面)来划分。这个文件夹里面包含改模块所有的资源。单一出口,就是改模块所有的输出都通过config.js文件导出;单一出入,就是外部调用通过只需引用config.js;这样做的好处在于,无论你的模块文件夹内部有多复杂,外部引用的时候,都是从一个入口文件引入,这样就很好的实现了隔离,如果后续有重构需求,你就会发现这种方式的优点
- 就近原则,紧耦合的文件应该放到一起,且以相对路径引用
好处在于方便整体模块的搬迁
- 公共文件应该以绝对路劲方式从根目录引用
如果我们需要对文件夹结构进行调整。将 /src/components/input 变成 /src/components/new/input,如果使用绝对路径,只需要全局搜索替换。而如果使用相对路径,则需要一个个找,很麻烦。
- /src外的文件不应该被引用
这一点就比较好理解了,而且其实已经有脚手架做了相关的约束了,正常我们的前端项目都会有个 src 文件夹,里面放着所有的项目需要的资源,js, css, png, svg 等等。src 外会放一些项目配置,依赖,环境等文件。所以,src 文件夹外不应该放需要被引入的资源。
这样的好处是方便划分项目代码文件和配置文件
- 问:有什么更好的方案和改进空间?
脚手架分析
目前我们主要应用到的工具就是webpack,就mpvue而言我们,主要做了些什么?
- 实现多端小程序的分包功能
// 分包json路径
exports.pathHandle = function (targetPath, absolutePath) {
let arr = absolutePath.toString().split('pages' + path.sep)
let fullPath = arr[1]
if (!fullPath) return targetPath
let packageName = fullPath.split(path.sep)[0]
let fileName = fullPath.split(path.sep)[2]
if (packageName === 'main') {
return targetPath
} else {
return packageName + path.sep + fileName
}
}
// 字节跳动小程序app.json的实现
exports.optimizeAppJson = function(content, path) {
if(process.env.PLATFORM !== 'tt') return content
const appJson = JSON.parse(content.toString('utf-8'))
if(appJson.subPackages && appJson.subPackages.length) {
let arr = []
appJson.subPackages.forEach((item) => {
item.pages && item.pages.forEach((child) => {
arr.push(item.root + child)
})
})
appJson.pages = appJson.pages.concat(arr)
delete appJson.subPackages
delete appJson.preloadRule
}
return JSON.stringify(appJson, null, 2)
}
- 重构官方的目录结构
// 获取entry路径
function getEntry (rootSrc, pattern, packageType) {
...getKey
}
function getKey(dirnames, packageType) {
...
}
// 整合入口文件信息
const appEntry = { app: resolve('./src/main.js') }
const pagesEntry = getEntry(resolve('./src'), 'pages/main/**/config.js', 'main')
const packageEntry = getEntry(resolve('./src'), 'pages/package*/**/config.js', 'sub')
const entry = Object.assign({}, appEntry, pagesEntry, packageEntry)
- 实现动态编译不同运行环境
const argv = process.argv.slice(2)
const params = {}
// 获取参数
argv.forEach((a) => {
const arr = a.split("=")
params[arr[0]] = arr[1]
})
// 导出需要的参数
module.exports = {
versions: params.ver || '',
applications: params.app || 'platform',
environments: params.env || 'production',
platforms: params.pla || 'wx',
[params.pla + 'Id']: params.id
}
- hygen
自动化创建页面模板,旨在提高开发效率
1. npm run new xxx // 执行hygen new _template/xxx 对应的prompt.js
2. // 符合要求后,会依次执行_template/xxx 下 xxx.ejs.t文件
3. // 自动生成对应的文件
- 脚手架还应该具备的特质
封装性(隔离性)
引用 《前端工程化:体系设计与实践》书里的一段话,“前端工程体系的功能涵盖范围广,封装的方案类型多,对应的配置项也非常复杂。而且,大多数前端工程体系的开发者并不是一线的业务开发者。对于业务开发者来说,这套工程体系就是一个黑盒,他们不需要了解其中的复杂原理,只需要知道如何配置即可。所以业务开发者的需求就是快速开发快速配置,并且生成的配置项跟项目要对应,既要满足项目的功能需求,又不能有“混淆视听”的冗余功能。”
封装和隔离,目的就是对非必要的工程化体系的技术细节封装到一个黑盒中,对一线业务开发人员从不必要的工程体系相关繁琐的工作开发任务隔离开来,提高相关业务人员关注的业务本身,提高专注性,提高生产效率。
兼容性
由于脚手架使用的特殊性,大部分使用场景是在工程师的本地平台上,所以,一个优秀的脚手架,就需要考虑到多个系统平台的支持,理论上讲不能有平台限制(除非这个脚手架是仅仅是服务你们内部项目并且你们公司的开发环境是高度统一的)
灵活性(可扩展性)
针对这个特性,想先强调是针对内部项目或内部业务而产生的客制脚手架,首先是我们的脚手架应该尽量具备灵活性和宽展性,那么怎么理解这点呢? 因为现在前端的多样性和复杂性,服务于我们自己业务项目的脚手架应该考虑之后业务变化带来的技术更替,要考虑技术兼容的问题,所以一个灵活的可配置的脚手架设计就会为今后的升级更新带来便利。在脚手架的设计之初就要考虑到这点,那么为了避免过度设计,建议参考下面几点建议。
1.考虑好你的脚手架的服务生命周期的目标,设计好每个生命周期阶段的任务,之后再向不同的生命周期的篮子里填充你的功能,尽量保持周期之间互不影响。
2.如引用第三方的模块,尽量引用第三方模块的原有功能接口,如有成熟的配置工具模块,尽量调用成熟的模块功能,这样在之后的技术更新中,如果第三方模块接口不变,我们就不需要做额外的工作做兼容。
3.前期尽量功能不要太多,只提供必要的功能支持,因为功能少就意味着依赖更少,灵活性可控。
业务和工程目标驱动性
补充这点,主要是强调一下脚手架最终的目的是为业务和工程服务的,应该是业务和工程驱动脚手架变化的,而不应该是工程师团队,它不是技术的练兵场和百宝箱,不是任何一个工程师觉得一个新技术或新点子不错,就把这些加入到我们的脚手架里面。脚手架的目的都是纯粹的,“服务业务,提高效率”。
目前任何一个和前端相关的库和框架,ReactJS,VueJS,Express等等的脚手架的目的都是统一和纯粹的,让用户快速上手熟悉它们的东西,降低框架自身的学习成本。推广它们的产品和业务。所以,一个好的脚手架,应该是为业务和工程服务的。
- 制作脚手架可能依赖的相关库
基础依赖工具
互动命令行工具库
命令行美颜工具
加载效果工具
npm多服务启动工具
Others
其实上面列出的只是帮助大家搭出脚手架骨架的工具类。如果大家要打造针对自己项目高度定制化真正核心的脚手架,需要的是大家对自己定制化的模板文件的构建和解析能力,其实真正依赖的还是你的webpack,Gulp等项目库的理解和掌握。
组件化
组件化讲起来是个非常简单的概念,前端主要的开发工作是 UI 开发,而把 UI 上的各种元素分解成组件,规定组件的标准,实现组件运行的环境就是组件化了。
现行的组件化方案,目前有五种主流选择:Web Component;Vue;React;Angular;自研。
- 组件具有什么样的特性
- 高度内聚,不透明 — 不需要关心这个东西怎么实现,知道怎么用。
- 对外以接口契约 — 有说明书,知道这个东西怎么用。
- 功能相对独立 — 相对概括地指明这个东西的使用途径。
- 环境依赖 — 对环境依赖比较重要。
- 可重用 — 可以多次使用。
- 可组装 — 可以和其他东西组装。
- 设计一套符合系统经需求的组件(面向需求的组件开发)
第一要素:以需求为中心,以满足需求为衡量标准
- 挖掘符合本系统的组件做基础组件和样本组件。
一个系统的组件系统很少是自己从头搭建,因为成本问题。在业务的快速迭代中,一般都会选择一些提供原子组件的第三方库和包,通过包装第三方的组件,形成符合本系统需求的组件系统。
- 构建需求组件,适应现有系统的组件模型和需求规范。
不同组件定义了自己不同的对外接口契约,千变万化的契约会带来杂、乱、差。不管哪一方的组件,乃至自己的组件,都需要遵守本系统的组件模型规范。
- 可选拓展,在需求可伸缩范围支持拓展和收缩。
在需求组件的开发中,对需求的发展,乃至回滚都有很清楚的认识。在这样的基础之上,可以在组件开发中留有可选拓展项,需求伸缩和回滚都能把控。
- 适可而止,不过多设计。
一切以需求为准,满足即是最好。
- 自描述性更高,组件设计对需求的耦合性更高,需要更多的描述。
因业务的复杂度耦合更多的独特规则,这样的组件更需要更高的自描述性内容。
- 设计一套可以通用的组件(面向复用的组件开发)
第一要素:以通用为中心,以抽象共性解决同类问题为衡量标准
- 关联稳定的领域抽象。
通用,即稳定,形成共识的领域。所以通用组件,也是稳定领域抽象出的组件。
- 将组件一般化。
一般组件都是为了解决特定问题而产生的应用,将这个应用进行一般化,关联更普遍的业务对象。
- 控制复杂度和可读性。
越通用的组件,可复用性越高,但是复杂度性对也越高,可读性也越差。
- 一致性。
统一的接口和契约,乃至统一对外异常暴露,只有大家越一致,更通用才会成为可能。
- 适应性。
为组件增加一个配置接口,为组件伸缩提供一种可能。
工具开发
随着需求不断的迭代和增加,我们会将能够大量重复应用到的功能提炼成工具,方便使用和维护;
工具最好是个纯函数;黑盒原则;方便使用
测试
- 单元测试
- 端到端测试
异常监控
我之前忽视了这方面的处理
- JS 异常处理
对于 Javascript 而言,我们面对的仅仅只是异常,异常的出现不会直接导致 JS 引擎崩溃,最多只会使当前执行的任务终止。
try-catch 异常处理
try {
error // 未定义变量
} catch(e) {
console.log('我知道错误了');
console.log(e);
}
但是 try-catch 处理异常的能力有限,只能捕获捉到运行时非异步错误,对于语法错误和异步错误就显得无能为力,捕捉不到。
window.onerror 异常处理
window.onerror 捕获异常能力比 try-catch 稍微强点,无论是异步还是非异步错误,onerror 都能捕获到运行时错误。
/**
* @param {String} msg 错误信息
* @param {String} url 出错文件
* @param {Number} row 行号
* @param {Number} col 列号
* @param {Object} error 错误详细信息
*/
window.onerror = function (msg, url, row, col, error) {
console.log('我知道错误了');
console.log({
msg, url, row, col, error
})
return true;
};
error;
关于 window.onerror 还有两点需要值得注意
对于 onerror 这种全局捕获,最好写在所有 JS 脚本的前面,因为你无法保证你写的代码是否出错,如果写在后面,一旦发生错误的话是不会被 onerror 捕获到的
另外 onerror 是无法捕获到网络异常的错误。
- 异常上报方式
- 通过 Ajax 发送数据
- 动态创建 img 标签的形式
function report(error) {
var reportUrl = 'http://xxxx/report';
new Image().src = reportUrl + 'error=' + error;
}