背景
目前新浪保险在微博内的商城是基于Vue的,但是和微博账号体系是绑定的,一定程度上限制了微博端外的拓展渠道,所以希望有单独的一套以便完全脱离微博用于端外拓展以及供给代理人使用。
该项目基于React从0到1,本文只记录开发中遇到的问题以及解决问题的思路和方法,不会过多的介绍某个技术栈的使用(后期会单独总结)和业务相关的内容。
React的优点之一便是组件化。由于每个险种detail有很多相似之处,所以全部抽离成组件,后期每增加一款险种根据自己的业务需求拼装即可,一些个性化的功能以及UI也做了相应的兼容性处理。
技术栈
react、react-dom、redux、react-redux、react-router-dom、scss、css-module、webpack、css-transition-group
实际上在前期搭建项目时还用到了redux中间件redux-saga,但在实际开发中没有使用。我希望在满足业务需求的同时尽可能的让项目简洁,这样无论是后期迭代还是他人维护都易于上手。
第三方UI库
ant-design-mobile
一、 创建项目
本项目基于React提供的脚手架开发。
- 全局安装create-react-app
npm i create-react-app -g
- 创建项目
create-react-app sina_insurance
- 运行
npm start
项目命令分别执行的是node_modules
下 react-scripts/script
中的相应文件。
如果要修改webpack的配置,可以通过执行该命令将配置文件弹射出来,需要注意的是,该操作是不可逆的,也就是说一旦弹射出来就无法恢复。
如果不想eject,还可以用react-app-rewired
。
该项目使用的是eject
- 弹射配置文件
npm run eject
执行完成后会多出两个目录config和scripts。config中是webpack相关的配置文件,scripts中是项目命令执行的相应文件。
执行完成后,重新启动项目。
二、 项目需要的依赖
- redux
- react-redux
- react-loadable 懒加载React组件,会将组建切割成单独的js文件
- axios
- node-sass sass-loader
- postcss-plugin-px2rem
- sass-resource-loader 全局混入sass需要用到该依赖
- babel-plugin-import 按需加载第三方组件
- ant-design-mobile 基于React的移动端UI库
- less less-loader UI库使用less
- classnames
三、 webpack配置
- 配置postcss-plugin-px2rem
在postcss-loader选项options的plugins中增加以下配置
require('postcss-plugin-px2rem')({
rootValue: 750/16,
unitPrecision: 5,
propBlackList: ['border', 'border-left', 'border-right', 'border-top', 'border-bottom'],
exclude: false,
selectorBlackList: [],
ignoreIdentifier: false,
replace: true,
mediaQuery: false,
minPixelValue: 0
})
关于border不转rem,一般机型dpr都是2倍或3倍,而且flexible.js中设置了scale为 1/dpr,所以会进行缩放。但是这样做对UI库不是太友好,需要修改antd-mobile/lib/style/thems/default.less
中基本单位修改成2px。
- 全局混入scss变量、函数,新增loader
定义sass变量和函数的文件几乎每个组件都会用到,而每次引入不但麻烦而且会造成代码冗余,可以通过webpakc全局混入,在任何地方都可以直接使用。
在getStyleLoaders函数最后新增如下配置:
if (preProcessor) {
const option = {
loader: require.resolve(preProcessor),
options: {
sourceMap: isEnvProduction && shouldUseSourceMap,
}
}
if (preProcessor === 'less-loader') {
// less svg
option.options.javascriptEnabled = true
}
loaders.push(option)
}
if (preProcessor === 'sass-loader') {
// 全局混入sass,一定要在sass-loader之后!!!否则会被sass-loader覆盖!!!
const sassResourcesLoader = {
loader: require.resolve('sass-resources-loader'),
options: {
resources: [
path.resolve(__dirname, '../src/assets/style/mixin.scss')
]
}
}
loaders.push(sassResourcesLoader)
}
说明:
- 增加loader
如果是less-loader, 由于项目使用的loader版本是4.1.0,而在less-loader3.x以后的版本需要添加javascriptEnabled: true
选项,否则在使用svg的icon时
会报如下错。(项目使用了三方库的loading icon)
- 增加
sass-resources-loader
用于在webpack配置中全局混入scss文件,但是一定要在sass-loader之后引入,否则会被sass-loader的配置覆盖。
- 添加alias
const resolvePath = (dir) => {
return path.resolve(__dirname, '../src', dir);
}
alias: {
'api': resolvePath('api'),
'assets': resolvePath('assets'),
'component': resolvePath('component'),
'views': resolvePath('views')
}
- 修改sourceMap配置
简单说,Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。
主要是便于开发人员debug。因为在开发和生产中,代码都会经过babel编译转换,处理后的代码和源代码差异很大,出现bug很难准确定位到问题的具体位置。其实在生产环境没必要生成该文件,反而会造成打包后的文件体积过大,所以只在生产生成sourceMap文件。修改配置如下:
- 添加测试环境变量
项目分三种环境,开发、测试、线上。每个环境的Api不同,项目中根据process.env.NODE_ENV仅能区分生产和开发环境。因为提交给QA的代码和生产是一样的,唯一的区别的是Api不同,我想要在打包发布给QA时自动识别出环境,设置相应的Api。
思路:
node执行js是可以带参数的,通过process.argv获取。process是一个全局对象,argv返回的是一组包含命令行参数的数组。
数组第一项为“node”,第二项为执行的js的完整路径,后面是附加在命令行后的参数。
无参数:
带参数
解决方式:
- 新增命令 执行build.js,并添加参数。
"int": "node scripts/build.js qa_env",
- 在build.js中判断是否有qa_env参数,如果有则修改process.env.NODE_ENV的值为qa_env
测试环境运行时报如下错误:
产生原因:
目前uglifyjs会在混淆代码的同时,更改一些变量、函数名,以减小js文件的体积。而redux利用了这点,判断当前环境。
尝试解决方式:
redux是根据 new webpack.DefinePlugin()
判断环境的。
而参数的值来自env.js
在执行npm run int时,process.env.NODE_ENV为'qa_env',redux会根据这个进行打包压缩。通过env.stringified获取的也是'qa_env',但是redux压缩编译时是识别不了该标识的,当成development压缩编译,所以在测试环境打包出来的体积会达到1.7+M,在项目运行时console会提示该警告,意思是包是压缩后的,但是redux是没压缩的,会导致项目运行缓慢。
太乐观,此种解决方式不可行。在编译时,项目js读取的都是new webpack.DefinePlugin中的prosecc.env中的值。
start.js中通过process.env.NODE_ENV定义的development 是运行时,在开发环境就是development。而打包编译时文件中通过process.env.NODE_ENV读取的是插件中重新生成的值。这样测试环境也是production。
四、 项目问题
- a标签添加
rel="noopener noreferrer"
属性
在没有rel=“noopener noreferrer"属性的a标签中使用target=”_blank"存在一定的风险”
当一个外部链接使用了target=_blank的方式,这个外部链接会打开一个新的浏览器tab,并且和原始页面占用同一个渲染(UI)进程。新标签页中 window.opener
指向上一个标签页的window。
noopener noreferrer
就是告诉浏览器,新打开的子窗口不需要访问父窗口的任何内容,这是为了防止一些钓鱼网站窃取父窗口的信息。
此外,如果这个新页面有任何性能上的问题,比如有一个很高的加载时间,这也将会影响到原始页面的表现。
如果你打开的是同域名(协议、域名、端口相同)的页面,那么你将可以在新页面访问到原始页面的所有内容,包括document对象(window.opener.document)。但是如果你打开的是跨域的页面,即便他们是同一站点(根域名和协议相等),你依然无法通过 opener 来操作父标签页中的 DOM,依然会受到同源策略的限制,但是你依然可以访问到location等对象。
<a rel="noopener noreferrer" target="_blank" href="https://www.baidu.com">
以上 a 链接的 rel 属性值都使用了 noopener 和 noreferrer,通常,将 noopener 的值引入 rel 属性中,就是告诉浏览器通过这个链接打开的标签页中的 opener 值设置为 null,引入 noreferrer 是告诉浏览器,新打开的标签页不要有引用关系(不要在同一浏览器上下文组)。
- 组件卸载取消未完成的异步操作
问题:
原因:
blur() {
// 让setState同步更新state
this.timer = setTimeout(() => {
this.setState({
showIcon: false
})
}, 10)
}
setTimeout是异步的,在组件卸载时可能仍然处于任务队列中。说明一点:setTimeout是React控制之外的,在其回调中执行的setState是同步的。如果不用setTimeout,setState就是异步更新。在组件卸载时,仍可能有未完成的异步任务。
fix:
componentWillUnmount() {
// 卸载组件取消未完成的异步事件,防止内存泄漏
this.setState = () => false
clearTimeout(this.timer)
}
- 阻止默认事件
开发mask组件时,需要在触发mask的touchmove事件时阻止底部body滚动。React要求阻止默认事件使用e.preventDefault(),而非return false。
但实际效果却事与愿违,调试中console不但有警告,而且没起作用。
根据 chrome 的提示,是因为 Chrome 现在默认把通过在 document 上绑定的事件监听器 passive 属性(后面细说)默认置为 true,这样就会导致设置的 e.preventDefault() 被忽视了。当然 Chrome 的这个做法是有道理,是为了提高页面滚动的性能,那么为了防止带来的副作用,官方考虑的很周到,给我们提供了一个 CSS 属性专门用来解决这个问题.
touch-action: none;
但这个属性在ios是不支持的,只能降级处理。
MDN解释:
根据规范,passive 选项的默认值始终为false。但是,这引入了处理某些触摸事件(以及其他)的事件监听器在尝试处理滚动时阻止浏览器的主线程的可能性,从而导致滚动处理期间性能可能大大降低。
为防止出现此问题,某些浏览器(特别是Chrome和Firefox)已将touchstart和touchmove事件的passive选项的默认值更改为true文档级节点 Window,Document和Document.body。这可以防止调用事件监听器,因此在用户滚动时无法阻止页面呈现。
浏览器默认设置 passive 为 true,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。
解决方案:
通过原生 addEventListener
给element注册监听事件
this.mask.addEventListener('touchmove', e => {
e.preventDefault()
}, {
passive: false
})