探讨点抛出
维护中的项目技术架构有些陈旧,如何切入新的技术框架
组件式开发提高代码质量,复用性,扩展性的一些技巧
与后端接口的联调效率问题
其他
架构更新 目前前端开发框架各项比较
非主流优秀框架简单介绍 : Svelte 、Mithril
接口数量及复杂 : Angular6 (500+) > Vue2 (100+) > React16 (50-)
国内开发者基础 : Vue ~= React > Angular
之前的协议问题导致不少开发者流出,大厂还是有大量优质组件是React开发的如 AntDesign
优秀的中文文档以及因有类似小程序这样的平台框架语法类似因此开发者数量较大,大厂也有Eleme跟AntV以及Vant等成熟开源组件库
有google背书 UI 有知名的 Material跟Ionic,其1.x的开发者还是不少,到2.x+后因不向下兼容强跟typescript结合开发流失了不少开发者。
框架附加组件量 : Angular > Vue > React
倾向企业级开发,方案都是集成度很高的,路由、Http、Form、Validator甚至打包(跟目前财人汇用到的F7有点类似)
讨巧的只引入了一般项目必要的组件,路由、状态控制、通讯用的observe,很轻量级因此能保持一个较小的体积,且能保证这些核心组件的稳定跟质量,虽然最大的问题是 Vue实际的主导者只有作者本人。
视开发者都是大神,提供了一个灵活的核心后其他的一切都随你去引入,这个导致的后果是很多初级开发者在react router这里就卡了壳 2.x、3.x、4.x、native、web ?想component根据路由按需加载?又得自己diy一个组件(当时自己被逼写的 )一个完整的React核心的项目因引入了大量开源社区质量程度不一的组件后导致项目稳定性需要一个相对熟悉这些组件的人去把控,难度变得较高。
组件颗粒程度 :Angular ~= Vue ~= React
都有一套自己的模板引擎,组件的引入也基本是需要在某处声明后再引入模板内。组件之间的通讯除了直接引用属性方法外都有一个EventEmitter的工具提供(Vue里是eventBus),解耦方面善用这个方式可以摆脱组件层级的限制。
有vdom因此模板引擎是不需要的,通过babel这类转义工具可以很方便的使用js原生的方式去做一些逻辑业务。组件通讯方面没有全局的监听工具因此需要自己引入。
个人开发感受 :
兼容性较好,性能较差,写法比较刻板(企业级框架的特色)2.x+因为后续没有继续使用没有更多的感受。
React15/16 + (Redux / Mobx) + AntD
分别开发过2个业务比较复杂的后台项目,搭建初期会消耗非常的大的精力在路由、加载策略以及状态管理选择上,React 的 state跟props 在大多数新手开发的时候都会遇到不少问题(刷新机制)且在性能上如果写法上稍有不注意就容易出问题,API少但灵活的框架对开发人员的要求其实更高,view层中频繁写入匿名函数是常见的一种偷懒写法,直接导致业务代码抽离能力减弱。
因为全家桶,大多数不需要细致定制的项目几乎都可以开箱即用。相对比较弱的应该是Vuex部分,相比其他API不太容易理解,mutation 跟 action 比较多的新手都没明白区别,在引入第三方的库上也没有很好的支持,不像React你真的理解不了Redux可以用相对容易理解的Mobx。好在大多数Vue项目用不到状态管理。缺陷是如果不是用class模式的组件在编辑器里没办法提示组件所提供的属性以及方法名,经常需要打开组件代码去看,效率上相对低一些。
综合考虑下的选择
NB目前所有的前端项目,排除服务端渲染的全部使用 Vue
为主力开发框架。理由:
引入的学习成本相对较小(React需要了解一堆其他的开源库、Angular则需要先熟悉TypeScript以及他那500+的API)
大量的成熟业务组件可复用(ElementUI,Vant…)
样式的超集选择比较灵活(lang=*来决定)以及作用域相对可控(加不加属性scoped),React的CSSInJS以及CSSModule依然需要很高的学习跟维护成本。
DIY一个组件的难度相对简单,React还区别 stateless
、purecomponent
等各种不停形态的组件以提升性能。
组件的写法方式较为统一,React因为是纯ESClass的方式,因此Webpack中Babel对应的ESstage决定了定义Class代码的样,举例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class App extends Component { constructor () { super (); this .state = { name: 'React' }; this .clickHandle = this .handleClick.bind(this ); } clickHandle() {}; render() { return (<div onClick ={this.clickHandle} > Hello, {this.state.name}</div > ); } } export default Form.create()(App);@Form.create() export default class App extends Component { state = { name: 'React' }; clickHandle = () => {}; render() { const { name } = this .state; return (<div onClick ={this.clickHandle} > Hello, {name}</div > ); } }
打包构建速度相对较快,打包后的体积也较小,在H5下比react有一定优势。React虽然可以通过其他方式减少体积(如preact等)但后遗症是一些组件稳定性跟兼容性会不可控。
老项目的迁移步骤
利用组件化的特性,在原有项目上做逐步的迁移。举例 F7框架项目,下面是步骤代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 var vueComponent = require ('gulp-vue-single-file-component' );var rename = require ('gulp-rename' );var babel = require ('gulp-babel' );gulp.task('vue' , function ( ) { return gulp .src('./src/components/src/**/*.vue' ) .pipe(vueComponent({ debug : true , loadCssMethod : 'loadCss' })) .pipe(babel({ plugins : 'transform-es2015-modules-amd' })) .pipe(rename({ extname : '.js' })) .pipe(gulp.dest('./src/components/public' )); }); require .config({ ... paths: { ... 'Vue' : '//cdn.jsdelivr.net/npm/vue/dist/vue' , '@' : '../components/public' }, ... }); define([ 'api' , 'Vue' , '@/navbar' , '@/testPage' ], function (Api, Vue, Navbar, TestPage ) { function init ( ) { document .querySelector('.navbar' ).innerHTML = '' ; var CNavbar = new Vue.extend(Navbar)({ el: document .querySelector('.navbar' ), propsData: { styleId: 0 , left: '<i class="icon icon-back"></i>' , title: '我的特权' , right: '会员规则' } }); CNavbar.$on('onRightClick' , function ( ) {...}); var CPage = new Vue.extend(TestPage)({ el: document .querySelector('.page-test' ), propsData: { Api } }); } return { init: init }; });
提高组件代码的质量
数据以及函数的依赖解耦(依赖越少,可定义的接口越多,可复用率越好)举例海通社区页面顶部navbar定义一个通用组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <template > <div class ="navbar-inner" :class ="{[classId]: true}" > <div class ="left" @click ="$emit('onBackClick')" > <slot name ="left" > </slot > </div > <div class ="center" > {{ title }}</div > <div class ="right" @click ="$emit('onRightClick')" > <slot name ="right" > </slot > </div > </div > </template > <script > export default { props: { classId: { type: String , default : 'default' }, title: { type: String , default : '' } } } </script > <template > <Navbar title ="我的特权" @on-left-click ="onLeftClick" @on-right-click ="onRightClick" :classId ="default" > <i class ="icon icon-back" slot ="left" > </i > <a href ="javascript:;" slot ="right" > 会员规则</a > </Navbar > </template > <script > export default { components: { Navbar: () => import ('Navbar.vue' ), }, methods: { onLeftClick() {}, onRightClick() {}, } } </script >
不要贪图一时的方便,将可能因环境变化而发生改动的数据放入可配置的js内。避免每次想切环境都要去修改对应的 举例,可根据打包参数进行转换的api地址前缀:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 let config = { apiPrefix: { ajzq: { test: 'http://118.xxx.129.xxx:8082' , production: 'https://nbapp.xxx.xxx:8890' , }, zhzq: { test: 'http://118.xxx.129.xxx:8082' , production: 'http://180.167.90.xxx:8082' , } }[__CLIENT__][__API__] }; new webpack.DefinePlugin({ __API__: JSON .stringify(process.env.API), __CLIENT__: JSON .stringify(process.env.CLIENT), }), "config" : { "env" : "production" , "client" : "ajzq" }, "scripts" : { "dev" : "cross-env NODE_ENV=development API=test CLIENT=$npm_package_config_client npx webpack-dev-server" , "build" : "cross-env NODE_ENV=$npm_package_config_env CLIENT=$npm_package_config_client webpack --mode production" }
如果组件内部的方法需要无视嵌入深度的,定义一个全局的eventbus来处理。改造之前的Navbar组件 注意点:这个事件的名称最好由一个字典配置管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template > <div class ="navbar-inner" :class ="{[classId]: true}" > <div class ="left" @click ="onLeftClick" > <slot name ="left" > </slot > </div > ... </div > </template > <script > import EventEmitter from 'EventEmitter' ;export default { methods: { onLeftClick() { EventEmitter.$emit('Navbar:onLeftClick' ); } } } </script >
不要过度颗粒化组件,这点跟React不同,React因为有stateless组件的概念组件的颗粒度越高效率也会越高。Vue并不建议这么做,并且在客户定制化变化频繁的情况下也无法实现。
1 2 3 4 5 6 7 8 9 export { Button: (props ) => <Button onClick ={props.onClick} > {props.text}</Button > } <template> <button @click="$emit('btnClick', $event)" >{{text}}</button> </ template>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 // 避免这样 <template > <UserList :dataset ="{{userList}}" /> </template > <script > export default { ... methods: { getUserlist() { axios.get(...).then(res => { this .userList = res; }); } } } </script > // 更好的方式 <script > import UserModel from 'model/user' ;export default { ... methods: { getUserlist() { UserModel.getUserlist().then(resp => { this .userList = res; }); }, getUserlistAsync: async () => { const res = await UserModel.getUserlist(); this .userList = res; } } } </script >
谨慎使用全局植入函数或属性,在财富 react 商城项目中看到 app.js 中包含了大量的 window.xxx。如何避免?引入前面讲到的一个能贯穿项目的事件观察机制(Observe,Vue里的EventBus)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 export default class { constructor () { this ._handlers = Object .create(null ); } fire(eventName, data) { let handlers = eventName in this ._handlers && this ._handlers[eventName].slice(); if (!handlers) return ; for (let i = 0 ; i < handlers.length; i += 1 ) { let handler = handlers[i]; if (!handler.__calling) { handler.__calling = true ; handler.call(this , data); handler.__calling = false ; } } } on(eventName, handler) { let handlers = this ._handlers[eventName] || (this ._handlers[eventName] = []); handlers.push(handler); return { cancel: function ( ) { var index = handlers.indexOf(handler); if (~index) handlers.splice(index, 1 ); } }; } } import Observe from 'libs/observe' ;const APIObserver = new Observe();axios({ ... }).then(res => { if (res.error_no == 304 ) { return APIObserver.fire('TOKEN_INVALID' , res); } if (res.error_no !== 0 ) { return APIObserver.fire('API_FAILED' , res); } APIObserver.fire('API_SUCCESS' , res); }); APIObserver.on('log' , message => { api.setLog(message); }); export { APIObserver }import APIObserver from 'APIObserver' ;mounted() || componentDidMount() { APIObserver.on('TOKEN_INVALID' , () => { routerTo('/login' ); APIObserver.fire('log' , 'redirect to login page.' ); }); }
Mock 数据 & 数据替换代理 mock 方法比较多样,目前看不太推荐将开关做到 config 中,直接通过本地代理重定位 api url 地址是比较好的办法,这样在开发接口部分可用部分不可用的时候 mock 具体哪几个接口可以做到颗粒度的控制,目前代理的工具比较新的是有赞的 zan proxy 还有一款使用起来复杂一些的类似库 whistle 。 请求数据的代理转发在前端开发中扮演比较重要的角色:
线上正式代码本地 hotfix (将正式js或者css替换成本地的文件,接口依然沿用正式的接口,做到修改bug上线能一把通过)
替换线上正式的index.html文件,在尾部追加类似 vconsole.js 这样的调试代码,实现 h5 在手机上的热调试(手机的wifi需要配置代理)
库预升级测试,升级某个库是否有风险,通过代理替换而无需去动源代码。
除了本地替换的方式外,也可以利用先上的一些工具,比如 easy-mock ,可以事先跟后端同学沟通好,将api route以及字段都在上面定义好,或者通过swagger导出的json结构自动生成对应的api,提高制作mock的效率。
前端在调用api的时候用的比较多的比如postman等工具,但为了提高效率,建议在编辑器内整合。比如 vscode中的 REST Client 插件,可以快速的通过定一个一个.http文件并定义几个不同参数的 http 请求快速的在编辑器 Tab中得到数据结果,并可以方便的复制粘贴这些数据。
项目结构
这个结构中,less,css,font,img 其实都是服务于修饰页面文档层面的,可做适当的优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 assets |- styles |- less |- css |- ... 其他的一些样式超集语言 |- img |- font libs |- models // 数据模型包含api的操作的数据整理等,从组件中抽离 |- vendor // 第三方引入的组件,除了 npm 方式以外如果需要对源码做微调的也可以放在此处 |- utils // 工具合集 components |- pages // 页面级别的组件(也可以称为容器组件) |- partials // 局部复用的UI组件(子容器,基本常见的header,bar等,也可以取名为layout,布局级别的组件) |- modules // 功能性复用的组件目录 |- pagination // 举例分页组件 |- assets // scoped 样式目录 里面存放 样式文件图片等跟组件一起独立的资源 |- pagination.vue // 这里如果还要再做子组件,也可以pagination-xxx.vue config // 如果配置文件过多可以单独起一个目录 |- clients // 可针对不同客户单独的配置 |- urls.js |- ..
项目结构的最低要求,未来扩展或者调整结构代码不需要大幅的调整,影响全局跟局部的文件目录最好有所区别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // webpack.config.js resolve: { extensions: ['.js', '.jsx', '.css', '.less', ...], modules: [path.resolve('node_modules'), path.resolve('src')], alias: { common: path.resolve('src/common'), utils: path.resolve('src/utils'), static: path.resolve('src/static') } } // 之后的引用就可以变为 import Utils from 'utils'; import General from 'common/general'; import AppConfig from 'utils/appConfig'; import FavoriteBtn from '@/favorite-btn'; // 好处:万一需要优化调整目录,不至于要逐个去修改引入路径,只需要调整webpack 的别名配置路径。
后端接口相关 excel文件不利于更新及时,且想要搜索或者看一个大概的数据结构非常不便, 建议后端采用 swagger 类似的方式,对于前端开发来讲很直观,提高效率,这个需要后端的同学推进下。