【互动业务中的工程化系列】—— 互动业务中的Rax
文末福利:618 团队实践小册
NO.1
概述
之前渚薰大佬写了篇文章《玩转娱乐化时代|淘系互动团队几年的技术沉淀+经验都在这!》正在孵化EVA这样一个“人人可开发,处处有互动”的互动整体解决方案,我主要负责的部分就是EVA Workstation(互动工作站):目标是互动研发提效。
在这个整体解决方案中我们面临很多工程化的问题,不仅要提效,更要让开发者用的顺手舒服,接下来我将会针对这个系列专题,分享一些我们的思考和方案。
NO.2
互动业务需要融入Rax体系
在去年的这个时间点,遇到的挑战是在互动未提出准化之前,大部分时间是在做活动,活动结束就下线,所以技术栈比较杂,基本技术选项都是基于自己的喜好。而也是在这个时间点,整个淘系技术部在做技术上的统一和整合,减少重复造轮子,所以其中一个重要的事情就是,技术栈收拢到年,而且后续大团队的各种基础设施都会基于Rax体系来发展,所以整个互动业务要标准化要提效,最重要的事情就是融入到整个大的技术体系,这样才能更好的使用大团队的各种基础设施平台,让专业的人做专业的事情。
NO.3
互动业务的特点和诉求
我们的互动业务特点是:
互动业务只需要考虑WEB环境,不需要兼容WEEX。在原有的Rax 0.x体系下是,Rax考虑的更多的是WEEX和WEB的双端兼容,并且为了兼容WEEX环境,Rax 0.x对WEB侧的书写有很多牺牲。
互动业务因为涉及到Canvas这样的游戏区域,所以性能侧要求会更高,需要把更多的性能留给Canvas区域。
所以基于上面的两个诉求,互动后续接入Rax体系的策略都是围绕着这两个诉求展开。那么具体到目前互动业务工程化和Rax体系的Gap有下面这几点:
与WEB开发一致的CSS书写体验:Rax为了兼容WEEX环境,牺牲了CSS在WEB下的很多能力,而这些牺牲了的CSS能力正是互动需要的:
内敛样式无法使用Autoprefix导致一些老系统的样式兼容性问题;
内敛样式的写操作性能相对外联样式性能较差;
我们需要用到vw/vh的能力;
需要使cssnext;
CSS自定义属性处理刘海屏问题。
希望在书写WEB的同时,也可以使用Rax生态的基础组件,这样后续我们输出的互动业务组件,是可以和其他团队互通的;
SVG图标的使用和优化;
基于上述的这样一些诉求和对Gap的分析,以及对Rax生态体系调研之后,整体的技术方案是:基础Rax脚手架 + build-plugin-rax-app + 互动业务自定义插件 + 互动业务的命令行集合工具。
build-plugin-rax-app
主要用到该插件的 inlineStyle": false
配置,这样就可以用上非inline样式。
并且只需要考虑支持WEB情况,所以
targets
配置为:"targets":
[
"web"
]
。
互动自定义插件
提供CSS Modules的能力。
PostCSS相关插件以及vw/vh、cssnext等能力。
SVG图标的使用和优化。
DefinePlugin定义一些不同的环境变量和资源版本等,方便做我们自己的埋点和数据监控。
NO.4
互动业务脚手架
基于上诉的一些问题拆解,再加上我们希望用户在本地初始化完成的模块工程就是一个开箱即用的状态,不用再去调整一些默认配置。所以我们决定在Rax基础脚手架之上去定制和扩展我们互动需要的能力:
基于 Rax 基础脚手架模板定制我们互动自己的脚手架;
在构建时基于规范@alib/build-script规范,扩展互动业务需要的互动构建插件;
并且互动脚手架还能带来一些额外的好处:
互动脚手架开箱即用:EVA JS使用demo、Rax EVA最佳实践、 CSS和JS书写最佳实践、ESLint、Stylelint、无障碍检测、Commit Message 规范等均内置;
通过提供@alib/build-scripts 的互动插件,编译符合互动业务诉求的产物:需要用到PostCSS相关插件、scoped-css-loader 解决样式冲突问题、DefinePlugin内置一些常用的环境变量、webpack-bundle-analyzer 用于分析产物大小,是否有公共资源需要抽取到externals等等功能;
HTML页面内置EVA JS游戏引擎、Rax EVA(提供游戏结点和DOM结点混合开发的能力)等互动需要的基础库。
一些互动业务开发时的常用方案接入:
Mock能力接入;
JS资源大小分析和优化;
自动化测试接入。
通过这样的方式,既能很好的融入Rax 体系,又能保持互动业务自己的特点和诉求。下面介绍下我在做互动脚手架过程中的遇到的一些CASE和解决思路。
脚手架基础配置
脚手架build.json
配置如下:
{
"inlineStyle": false,
"plugins": [
[
"build-plugin-rax-app",
{
"targets": ["web"]
}
],
[
"./builder/myPlugin.js",
{
"svgConfig": {
"source": "./src/assets/icons"
}
}
]
]
}
./builder/myPlugin.js"
中的postcss核心配置代码片段
plugins: () => [
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
features: {
'nesting-rules': true,
},
}),
require('postcss-plugin-rpx2vw')()
]
package.json
中的浏览器兼容性配置,Autoprefix 会读取该配置:
"browserslist": [
"iOS >= 4",
"Firefox >= 20",
"Android > 4.0"
]
上述的这些基础内容配置完成后,我们的脚手架已经可以跑起来了。接下来我们可以进一步做一些优化,给我们带来更好的开发体验。
CSS Modules技术选型
我们开始的技术方案是使用[css-loader 的modules 配置,但是这个配置存在的问题是,处理全局的样式还需要单独配置loader处理,并且 className
的书写方式也不是那么舒服,这种写法对代码侵入性太强,带来的开发体验并不好。书写时,以index.tsx
为例,这样的书写方式还是不那么舒服的。
import '@assets/style/base.css?raw';
import styles from './index.css';
function Button(props){
return <button className={styles.testBtn + " button"}>
click index page
</button>
}
所以后来我们发现scoped-css-loader 这个非常优秀的loader和babel-plugin-react-scoped-css 配合一起使用,能带来更好的书写体验,而且对代码侵入性很低:
webpack.config.js
配置
/<strong> webpack.config.js </strong>/
{
// 专门处理非src的css文件,比如node_modules里的css文件
test: /\.css$/i,
use: [
isProd ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
'postcss-loader',
{ loader: 'scoped-css-loader' }
]
},
.babelrc
配置
"plugins": [
...
[
"@babel/plugin-transform-react-jsx",
{
"pragma": "createElement"
}
],
"babel-plugin-react-scoped-css"
]
index.tsx
import '@assets/style/base.css';
import './index.scoped.css';
function Button(props){
return <button className="testBtn button">
click index page
</button>
}
我们可以看到 scoped-css-loader 和 babel-plugin-react-scoped-css 给我们的dom加上了 data-v-xxx
这样的属性,样式选择器也通过这样的方式实现了scoped的效果。
遇到的问题
在使用 babel-plugin-react-scoped-css 的时候我们也遇到了一个问题,如果是模块开发的方式。不同的模块会用相同的className
,而且babel-plugin-react-scoped-css 在1.0.0版本之前,作者是通过filename
来生成data-v-(hash)
的hash,这样虽然保证了在同一个工程里className
不会冲突,但是模块开发在不同的工程中,文件名就很容易冲突。所以后来和作者进行了一番讨论,通过给babel-plugin-react-scoped-css 提供hashSeed
这样一个配置,解决这个问题,所以在提交pr之后,作者在1.1.0版本修复了该问题。pr核心逻辑如下:
- const hash = computeHash(stats.file.opts.filename)
+ const { hashSeed } = stats.opts
+ const hash = computeHash(hashSeed,stats.file.opts.filename)
JSX的问题
这个问题需要单独拿出来讨论的原因是,大部分业务场景为了兼容weex的环境,在写Rax 时只用rax-view、rax-text、rax-image、rax-picture等这类 Rax 的基础标签,但是互动的业务目前主要是考虑WEB的环境,这些标签使用时会带来一些问题,比如rax-view这样的标签有一些额外的扩展能力,虽然提供了onAppear
和onDisappear
等这样一类方便埋点的属性,但在渲染为div时,会加入很多额外的样式信息,比如下面这样:
<div style="border: 0px solid black; box-sizing: border-box; display: flex; -webkit-box-orient: vertical; flex-direction: column; align-content: flex-start; flex-shrink: 0;"></div>
这类样式信息就对我们的样式书写带来了干扰,所以最后我们经过一系列讨论之后,决定还是使用原生的dom标签来书写jsx,Rax 的这些基础标签只有在我们有对其能力有特殊需要的时候再进行使用,所以整个开发体验其实和React是很类似的,比如:
<View className="wrap" onAppear={handleAppear} onDisappear={handleDisappear}>
<button className="testBtn button">
click index page
</button>
</View>
并且rax-types 的类型定义已经比较完善,这样我们写TS的时候更加得心应手。
SVG的使用和优化
SVG主要用来处理一些图标,我们会用到svg-sprite-loader、svgo-loader。svgo-loader 主要用于SVG的压缩,svg-sprite-loader 主要是可以做到ID随处复用以及合成类似雪碧图的效果。
那么myPlugin.js
的配置为:
chainWebpack((config) => {
// build-plugin-rax-app 会把svg通过image-source-loader来处理,所以这里是覆盖掉image-source-loader处理svg的逻辑
config.module
.rule('assets')
.test(/\.(png|webp|jpe?g|gif)$/i)
.use('source')
.loader(require.resolve('image-source-loader'));
config.module
.rule('svg')
.test(/\.svg$/)
.include
.add(path.join(process.cwd(), svgConfig.source))
.end()
.use('svg-sprite')
.loader(require.resolve('svg-sprite-loader'))
.options({
symbolId: 'icon-[name]',
})
.end()
.use('svgo')
.loader(require.resolve('svgo-loader'))
.options(svgConfig.svgoConfig || defaultSvgoConfig);
});
在页面的page/Home.tsx
中我们还需要引入这样一段代码:
const requireAll = requireContext => requireContext.keys().map(requireContext);
const req = require['context']('../../assets/icons', false, /\.svg$/);
requireAll(req);
上面这段代码我们可以看下编译出来的结果
/***/ "./src/assets/icons sync \\.svg$":
/*!***************************************************!*\
!*<strong> ./src/assets/icons sync nonrecursive \.svg$ </strong>*!
\***************************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
var map = {
"./back.svg": "./src/assets/icons/back.svg",
"./roundclose.svg": "./src/assets/icons/roundclose.svg",
"./trophy.svg": "./src/assets/icons/trophy.svg",
"./water.svg": "./src/assets/icons/water.svg"
};
function webpackContext(req) {
var id = webpackContextResolve(req);
return __webpack_require__(id);
}
function webpackContextResolve(req) {
if(!__webpack_require__.o(map, req)) {
var e = new Error("Cannot find module '" + req + "'");
e.code = 'MODULE_NOT_FOUND';
throw e;
}
return map[req];
}
webpackContext.keys = function webpackContextKeys() {
return Object.keys(map);
};
webpackContext.resolve = webpackContextResolve;
module.exports = webpackContext;
webpackContext.id = "./src/assets/icons sync \\.svg$";
/***/ }),
/***/ "./src/assets/icons/back.svg":
/*!***********************************!*\
!*<strong> ./src/assets/icons/back.svg </strong>*!
require.context
帮我们创建一个上下文,在这里我们的上下文就是src/assets/icons
, 然后我们就可以用request.resolve('./back.svg')
来引用到back.svg
这个上下文中的SVG。而webpackContext.keys
则是将icons目录下的所有SVG都添加到上下文中。到这里还没结束,我们还需要创建一个Icon这样的通用组件,组件的逻辑是:
function Icon(props: IconProps) {
const { type, width, height, ...rest } = {
...defaultProps,
...props,
};
return (
<svg
width={width}
height={height}
fill="currentColor"
{...rest}
xmlns="http://www.w3.org/2000/svg"
dangerouslySetInnerHTML={{
__html: `<use xlink:href="#icon-${type}" href="#icon-${type}"></use>`,
}}>
</svg>
);
}
细心的同学可能会发现我在SVG中使用了 dangerouslySetInnerHTML
,React中可以把 xlinkHref
转换为 xlink:href
,但是Rax中是不支持 xlinkHref
转换为 xlink:href
的,这样就会导致我们SVG不出现,所以这里使用 dangerouslySetInnerHTML
来规避Rax的这个问题。
使用的时候直接用文件名作为Icon组件的type值就好,效果就如截图所示。
<Icon type="back" className="svg" />
互动业务命令行集合工具
在脚手架中会用到很多命令行,比如Commit Message规范、ESLint、Stylelint、Prettier等这类命令行的配置,这部分内容有2个相同的特点:
都是通过命令行来处理;
如果直接一股脑的加在脚手架里,每个工程中都留一份配置,不利于规则和配置一升级,可能造成合并时的冲突。
所以基于上面特点,为了能够解耦这些命令行和脚手架之间的关系,我抽离了一些常用命令行,通过Commander 整合到同一个npm包,收拢到eva xxx -x ...
这个命令下,例如Lint、Prettier、upload这类命令就整合到这个npm中,这个工具包接入脚手架时,整体的升级和配置都内置到这个工具包中。这个工具包中还提供了一些额外选项方便业务进行扩展;并且提供了插件机制,可以方便其他命令的扩展,提供插件的同学只需要按照我提供的插件规范约定,实现他的npm包命令行功能后,将这个npm提供给我,我加入到依赖当中。这样插件和工具集本身也实现了解耦,可以分开进行维护和升级。
一个小细节
在将ESLint和Prettier整合到同一个npm包时候遇到一个问题,就是Prettier的配置文件.prettierrc.js
,是需要放在当前工程的目录下才会生效,而如果需要使用ESLint的--fix
进行自动修复。这时候我们只需要按照eslint-plugin-prettier 的文档,将.prettierrc.js
里的内容配置.eslintrc.js
文件中的如下位置就好:
rules: {
'prettier/prettier': [
'error',
{
"printWidth": 100,
"tabWidth": 2,
"parser": "typescript",
...
}
],
...
}
完成上面的配置之后,工程目录下就不需要.prettierrc.js
这个文件,这样大家的格式化和Lint都来自于同一个npm包,然后Lint再和[Husky + commitizen 一起配合使用,代码提交时进行强制Lint和格式化,这样就实现了代码风格的一致性。package.json
中配置Husky 和 Commit Message规范:
"scripts": {
...
"commit": "git-cz"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint",
"commit-msg": "eva verify -E HUSKY_GIT_PARAMS"
}
}
webpack-bundle-analyzer
webpack-bundle-analyzer 就不详细介绍了,对着官方文档配置一遍就好。主要是用来分析我们的构建产物,方便我们对bundle进行优化。
NO.5
小结
所以我们基于Rax的标准,在Rax的基础之上,探索除了这样一个对我们互动业务写WEB比较友好的开发方式,通过扩展体系标准、补齐差异和局部定制的方式,实现了和Rax体系的对接,让互动的业务融入到Rax体系之中,这样在后续能更好的去使用Rax体系的各种平台,提高我们的生产效率。
NO.6
人人可开发、处处有互动
到文末了,不免想打个小广告,淘系技术部前端互动团队的使命是“人人可开发、处处有互动”。基于这样的目标,我们会提供统一的互动产品和研发平台来支撑互动业务的研发。同时整个淘系技术部前端团队,以技术驱动商业发展,不设界限的在前端相关领域深入突破,如互动、搭建、AI、Node、Serverless、中后台、云研发、终端技术等领域,以技术探索未来。
如果你也想一起定义未来,就快点加入我们一起改变世界吧!简历投递到邮箱:zhuxun.jb@alibaba-inc.com。
NO.7
参考文章
Rax官网(https://rax.js.org/?spm=ata.13261165.0.0.19de2fb598WAry)
react-scoped-css git仓库(https://github.com/gaoxiaoliangz/react-scoped-css)
懒人神器:svg-sprite-loader实现自己的Icon组件(https://segmentfault.com/a/1190000015367490)
ESLint官网——Command Line Interface(https://eslint.org/docs/user-guide/command-line-interface)
Stylelint命令行(https://stylelint.docschina.org/user-guide/cli/)
eslint-plugin-prettier文档(https://github.com/prettier/eslint-plugin-prettier#options)
Commander git仓库(https://github.com/tj/commander.js)
Git commit message 规范(https://juejin.im/post/5d0b3f8c6fb9a07ec07fc5d0)
Husky git仓库(https://github.com/typicode/husky)
webpack-bundle-analyzer(https://www.npmjs.com/package/webpack-bundle-analyzer)
回复 “618” 有福利
欢迎关注东半球最大的前端团队
喜欢就点这里