├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .travis.yml ├── .yarnrc ├── LICENSE ├── README.md ├── babel.config.js ├── build ├── rollup.config.base.js ├── rollup.config.browser.js └── rollup.config.es.js ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── modules.js ├── paths.js ├── pnpTs.js ├── webpack.config.js └── webpackDevServer.config.js ├── docs ├── components │ ├── code-snippet.jsx │ └── table.jsx ├── dist │ ├── asset-manifest.json │ ├── favicon.ico │ ├── manifest.json │ ├── precache-manifest.dcaea9017ecce0a42817a4f64187c726.js │ ├── service-worker.js │ └── static │ │ ├── css │ │ ├── 2.bc3eb4e0.chunk.css │ │ ├── 2.bc3eb4e0.chunk.css.map │ │ ├── main.fb47905e.chunk.css │ │ └── main.fb47905e.chunk.css.map │ │ └── js │ │ ├── 2.c5deaec1.chunk.js │ │ ├── 2.c5deaec1.chunk.js.map │ │ ├── main.74b83602.chunk.js │ │ ├── main.74b83602.chunk.js.map │ │ ├── runtime~main.936f90e7.js │ │ └── runtime~main.936f90e7.js.map ├── index.html ├── index.js ├── router │ └── index.js ├── store-mobx │ ├── app.js │ └── index.js ├── store │ ├── actions.js │ ├── index.js │ └── reducer.js ├── styles │ ├── animation.scss │ ├── common.scss │ ├── global.scss │ ├── mixins.scss │ ├── reset │ │ ├── index.scss │ │ └── normalize.scss │ └── variables.scss ├── tracks │ ├── action.js │ ├── events │ │ └── home.js │ └── index.js ├── utils │ ├── date.js │ └── logger.js └── views │ ├── block-show.jsx │ ├── home-mobx │ └── index.js │ ├── home-redux │ └── index.js │ ├── page-init.jsx │ ├── started.jsx │ └── trigger.jsx ├── jest.config.js ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── scripts ├── build.js ├── start.js └── test.js ├── src ├── index.js └── utils │ ├── dom.js │ ├── error.js │ ├── helper.js │ └── vis-monitor.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-14 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Sunday 2019-07-14 01:12:41 am 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | module.exports = { 13 | root: true, 14 | env: { 15 | node: true 16 | }, 17 | extends: "react-app", 18 | plugins: ["prettier"], 19 | rules: { 20 | "prettier/prettier": "error", 21 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 22 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", 23 | "exclude": /(dist)/ 24 | }, 25 | parserOptions: { 26 | parser: "babel-eslint" 27 | } 28 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | /dist 8 | 9 | # testing 10 | /coverage 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.yarnpkg.com -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | cache: 4 | yarn: true 5 | 6 | node_js: 7 | - '10' 8 | - '8' 9 | 10 | branches: 11 | only: 12 | - master 13 | 14 | install: 15 | - yarn 16 | 17 | script: 18 | - yarn lint 19 | 20 | after_success: 21 | - yarn build 22 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.yarnpkg.com" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 LHammer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # r-track 2 | 3 | Travis (.org) branch 4 | Gzip Size 5 | 6 | 7 | 8 | r-track是一个基于装饰器的埋点业务插件,可实现埋点代码与业务代码完全解耦,适用于react项目~ 9 | 10 | ## 安装 11 | 12 | ### YARN 13 | 14 | ```shell 15 | $ yarn add r-track 16 | ``` 17 | 18 | ### NPM 19 | 20 | ```shell 21 | $ npm install r-track --save 22 | ``` 23 | 24 | ## 用法 25 | 26 | r-track 包含track、inject两个方法,基本用法如下: 27 | 28 | ### inject 29 | 30 | 注入当前模块(页面)相关的埋点声明~ 31 | 32 | - **参数** 33 | - `{Object} trackEvents` 埋点事件声明的对象 [events](https://github.com/l-hammer/r-track/blob/master/docs/tracks/events/home.js) 34 | 35 | - **用法** 36 | ```js 37 | // react class component 38 | import { inject } from "r-track"; 39 | import tracks from "@/tracks"; 40 | @inject({ trackEvents: tracks.home }) 41 | 42 | // mobx class 43 | import { inject } from "r-track"; 44 | import tracks from "@/tracks"; 45 | @inject({ trackEvents: tracks.home }) 46 | 47 | // react class component + mobx-react 48 | import { inject } from "mobx-react"; 49 | import tracks from "@/tracks"; 50 | @inject(store => ({ store, trackEvents: tracks.home })) 51 | ``` 52 | 53 | ### track 54 | 55 | 埋点装饰器,对应 v-track 中的指令~ 56 | 57 | - **参数** 58 | - `{String} modifier` 对应的埋点类型 59 | - `{Number | String} eventId` 埋点事件id 60 | - `{Object} params` 自定义参数,目前支持delay、ref 61 | 62 | - **用法** 63 | ```js 64 | import { track } from "r-track"; 65 | 66 | @track("UVPV") 67 | @track("TONP") 68 | @track("trigger", 22121) 69 | @track("trigger.after", 22121) 70 | @track("trigger.once", 22121) 71 | @track("trigger.after.once", 22121) 72 | @track("async", 22121) 73 | @track("async.once", 22121) 74 | @track("async.delay", 22121, { delay: 3000, ref: "elementRef") 75 | ``` 76 | 77 | ## 示例 78 | 79 | 假如我们有一个 Home 的组件,需要在点击`button`时上报 id 为22121的埋点,示例如下: 80 | 81 | ```js 82 | import { home as trackEvents } from "@/tracks"; 83 | import { track, inject } from "r-track"; 84 | 85 | @inject({ trackEvents }) 86 | class Home extends Component { 87 | state = { 88 | date: Date.now(), 89 | }; 90 | 91 | @track("trigger", 22121) 92 | handleClick(val, e) { 93 | this.setState({ 94 | date: Date.now() 95 | }); 96 | } 97 | 98 | render() { 99 | return ( 100 | 101 | ); 102 | } 103 | } 104 | ``` 105 | 106 | 如果需要在`button`点击事件发生后上报埋点,可增加`after`修饰符,示例如下: 107 | 108 | ```js 109 | @inject({ trackEvents }) 110 | class Home extends Component { 111 | ... 112 | @track("trigger.after", 22121) 113 | handleClick(val, e) { 114 | this.setState({ 115 | date: Date.now() 116 | }); 117 | } 118 | ... 119 | } 120 | ``` 121 | 122 | 如果`button`点击事件包含异步请求,可使用`async`修饰符,示例如下: 123 | 124 | ```js 125 | @inject({ trackEvents }) 126 | class Home extends Component { 127 | ... 128 | @track("async", 22121) 129 | handleClick = async (val, e) => { 130 | const response = await new Promise(resolve => { 131 | setTimeout(() => { 132 | resolve({ date: Date.now() }); 133 | }, 300); 134 | }); 135 | 136 | this.setState({ 137 | date: response.date 138 | }) 139 | } 140 | ... 141 | } 142 | ``` 143 | 144 | UVPV、TONP(页面停留时长)埋点,示例如下: 145 | 146 | ```js 147 | @inject({ trackEvents }) 148 | class Home extends Component { 149 | ... 150 | @track("UVPV") 151 | @track("TONP") 152 | componentDidMount() { 153 | ... 154 | } 155 | ... 156 | } 157 | ``` 158 | 159 | 页面初始化异步埋点延迟上报,示例如下: 160 | 161 | ```js 162 | @inject({ trackEvents }) 163 | class Home extends Component { 164 | ... 165 | @track("async.delay", 22122, { delay: 3000 }) 166 | initContent = async val => { 167 | const response = await new Promise(resolve => { 168 | setTimeout(() => { 169 | resolve({ data: "success" }); 170 | }, 300); 171 | }); 172 | 173 | this.setState({ 174 | content: response.data 175 | }); 176 | }; 177 | ... 178 | } 179 | ``` 180 | 181 | > `async.delay`类型的埋点应该装饰在react component中,因为我们需要在页面卸载的时候及时清除定时器 182 | 183 | 区域或者元素曝光埋点,需通过 data-track-attributes 传递参数,示例如下: 184 | 185 | ```js 186 | @inject({ trackEvents }) 187 | class Home extends Component { 188 | constructor(props) { 189 | super(props); 190 | this.cardTrackRef = null; 191 | } 192 | 193 | @track("show") 194 | render() { 195 | return ( 196 | (this.cardTrackRef = ref)} 200 | className="block_show__card" 201 | > 202 |

我想被曝光

203 |
204 | ); 205 | } 206 | } 207 | ``` 208 | 209 | > 区域曝光类型的埋点需要元素绑定`ref`属性,属性必须以`TrackRef`结尾,即`ref={ref => (this.buttonTrackRef = ref)}` 210 | 211 | ## LICENSE 212 | 213 | [MIT](https://github.com/l-hammer/r-track/blob/master/LICENSE) © 2019-present, LHammer 214 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-14 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Monday 2019-08-19 18:38:10 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | module.exports = { 13 | presets: ["react-app"], 14 | plugins: [ 15 | [ 16 | "@babel/plugin-proposal-decorators", 17 | { 18 | legacy: true 19 | } 20 | ], 21 | [ 22 | "import", 23 | { 24 | libraryName: "antd", 25 | libraryDirectory: "lib", 26 | style: "css" 27 | } 28 | ] 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /build/rollup.config.base.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-08 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Friday 2019-08-23 16:23:28 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import babel from "rollup-plugin-babel"; 13 | import resolve from "rollup-plugin-node-resolve"; 14 | import commonjs from "rollup-plugin-commonjs"; 15 | 16 | export default { 17 | input: "src/index.js", 18 | plugins: [ 19 | babel({ 20 | runtimeHelpers: true, 21 | exclude: "node_modules/**" 22 | }), 23 | resolve(), 24 | commonjs() 25 | ], 26 | external: ["react-dom"] 27 | }; 28 | -------------------------------------------------------------------------------- /build/rollup.config.browser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-08 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Monday 2019-07-08 18:53:03 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import base from "./rollup.config.base"; 13 | import { uglify } from "rollup-plugin-uglify"; 14 | import replace from "rollup-plugin-replace"; 15 | 16 | const config = Object.assign({}, base, { 17 | output: { 18 | name: "RTrack", 19 | file: "dist/r-track.min.js", 20 | format: "umd" 21 | } 22 | }); 23 | 24 | config.plugins = [ 25 | ...config.plugins, 26 | uglify({}), 27 | replace({ 28 | "process.env.NODE_ENV": JSON.stringify("production") 29 | }) 30 | ]; 31 | 32 | export default config; 33 | -------------------------------------------------------------------------------- /build/rollup.config.es.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-08 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Monday 2019-07-08 18:53:09 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import base from "./rollup.config.base"; 13 | 14 | const config = Object.assign({}, base, { 15 | output: { 16 | name: "r-track", 17 | file: "dist/r-track.esm.js", 18 | format: "es" 19 | } 20 | }); 21 | 22 | export default config; 23 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebook/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 50 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in Webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | } 81 | ); 82 | // Stringify all values so we can feed into Webpack DefinePlugin 83 | const stringified = { 84 | 'process.env': Object.keys(raw).reduce((env, key) => { 85 | env[key] = JSON.stringify(raw[key]); 86 | return env; 87 | }, {}), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFileName = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFileName}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /config/modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | 8 | /** 9 | * Get the baseUrl of a compilerOptions object. 10 | * 11 | * @param {Object} options 12 | */ 13 | function getAdditionalModulePaths(options = {}) { 14 | const baseUrl = options.baseUrl; 15 | 16 | // We need to explicitly check for null and undefined (and not a falsy value) because 17 | // TypeScript treats an empty string as `.`. 18 | if (baseUrl == null) { 19 | // If there's no baseUrl set we respect NODE_PATH 20 | // Note that NODE_PATH is deprecated and will be removed 21 | // in the next major release of create-react-app. 22 | 23 | const nodePath = process.env.NODE_PATH || ''; 24 | return nodePath.split(path.delimiter).filter(Boolean); 25 | } 26 | 27 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 28 | 29 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is 30 | // the default behavior. 31 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { 32 | return null; 33 | } 34 | 35 | // Allow the user set the `baseUrl` to `appSrc`. 36 | if (path.relative(paths.appSrc, baseUrlResolved) === '') { 37 | return [paths.appSrc]; 38 | } 39 | 40 | // Otherwise, throw an error. 41 | throw new Error( 42 | chalk.red.bold( 43 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 44 | ' Create React App does not support other values at this time.' 45 | ) 46 | ); 47 | } 48 | 49 | function getModules() { 50 | // Check if TypeScript is setup 51 | const hasTsConfig = fs.existsSync(paths.appTsConfig); 52 | const hasJsConfig = fs.existsSync(paths.appJsConfig); 53 | 54 | if (hasTsConfig && hasJsConfig) { 55 | throw new Error( 56 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' 57 | ); 58 | } 59 | 60 | let config; 61 | 62 | // If there's a tsconfig.json we assume it's a 63 | // TypeScript project and set up the config 64 | // based on tsconfig.json 65 | if (hasTsConfig) { 66 | config = require(paths.appTsConfig); 67 | // Otherwise we'll check if there is jsconfig.json 68 | // for non TS projects. 69 | } else if (hasJsConfig) { 70 | config = require(paths.appJsConfig); 71 | } 72 | 73 | config = config || {}; 74 | const options = config.compilerOptions || {}; 75 | 76 | const additionalModulePaths = getAdditionalModulePaths(options); 77 | 78 | return { 79 | additionalModulePaths: additionalModulePaths, 80 | hasTsConfig, 81 | }; 82 | } 83 | 84 | module.exports = getModules(); 85 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | // const url = require("url"); 4 | 5 | // Make sure any symlinks in the project folder are resolved: 6 | // https://github.com/facebook/create-react-app/issues/637 7 | const appDirectory = fs.realpathSync(process.cwd()); 8 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 9 | 10 | const envPublicUrl = process.env.PUBLIC_URL; 11 | 12 | // function ensureSlash(inputPath, needsSlash) { 13 | // const hasSlash = inputPath.endsWith("/"); 14 | // if (hasSlash && !needsSlash) { 15 | // return inputPath.substr(0, inputPath.length - 1); 16 | // } else if (!hasSlash && needsSlash) { 17 | // return `${inputPath}/`; 18 | // } else { 19 | // return inputPath; 20 | // } 21 | // } 22 | 23 | const getPublicUrl = appPackageJson => 24 | envPublicUrl || require(appPackageJson).homepage; 25 | 26 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 27 | // "public path" at which the app is served. 28 | // Webpack needs to know it to put the right -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./router"; 4 | 5 | import "@/styles/global.scss"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | -------------------------------------------------------------------------------- /docs/router/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-14 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Thursday 2019-08-22 16:51:31 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import React from "react"; 13 | import { Prompt } from "react-router"; 14 | import { HashRouter, Route, Link } from "react-router-dom"; 15 | import Started from "@/views/started"; 16 | import Trigger from "@/views/trigger"; 17 | import PageInit from "@/views/page-init"; 18 | import BlockShow from "@/views/block-show"; 19 | 20 | const routes = [ 21 | { 22 | path: "/", 23 | component: Started 24 | }, 25 | { 26 | path: "/trigger", 27 | component: Trigger 28 | }, 29 | { 30 | path: "/page-init", 31 | component: PageInit 32 | }, 33 | { 34 | path: "/block-show", 35 | component: BlockShow 36 | } 37 | ]; 38 | 39 | function RouteWithSubRoutes(route) { 40 | return ( 41 | } 45 | /> 46 | ); 47 | } 48 | 49 | export default () => { 50 | return ( 51 | 52 |
53 |
54 |

r-track

55 |
56 | yarn add r-track or npm install r-track --save 57 |
58 | 94 |
95 | 🕹一个基于装饰器 & React的埋点业务插件 96 |
97 | 98 | 快速开始 99 | 100 | 101 | 事件行为埋点 102 | 103 | 104 | 页面行为埋点 105 | 106 | 107 | 区域展现埋点 108 | 109 | 110 | 打开一个 issue 111 | 112 |
113 | true}> 114 | {routes.map((route, i) => ( 115 | 116 | ))} 117 |
118 |
119 | Copyright © 2019-present LHammer 120 |
121 |
122 |
123 |
124 | ); 125 | }; 126 | -------------------------------------------------------------------------------- /docs/store-mobx/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-13 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Monday 2019-08-12 21:43:57 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import { observable, action, runInAction, reaction } from "mobx"; 13 | import { home as trackEvents } from "@/tracks"; 14 | import { track, inject } from "../../"; 15 | 16 | @inject({ 17 | reaction, 18 | trackEvents 19 | }) 20 | class App { 21 | @observable userInfo = {}; 22 | @observable date = Date.now(); 23 | @observable target = null; 24 | @observable content = null; 25 | 26 | @action.bound 27 | @track("trigger.after", 22121) 28 | handleClick(val, e) { 29 | console.log("handleClick 方法正常执行。并受到参数:", val, e.target); 30 | this.date = Date.now(); 31 | this.target = e.target; 32 | } 33 | // @action.bound 34 | // handleClick = async (val, e) => { 35 | // e.persist(); 36 | // const response = await new Promise(resolve => { 37 | // setTimeout(() => { 38 | // resolve({ date: Date.now() }); 39 | // }, 300); 40 | // }); 41 | // console.log('handleClick 方法正常执行。并受到参数:', val, response.data) 42 | 43 | // runInAction(() => { 44 | // this.date = response.date; 45 | // this.target = e.target; 46 | // }); 47 | // } 48 | 49 | @action 50 | @track("async", 22120) 51 | fetchUserInfo = async () => { 52 | const rest = await new Promise(resolve => { 53 | setTimeout(() => { 54 | resolve({ name: "LHammer", age: "18" }); 55 | }, 100); 56 | }); 57 | 58 | console.log("fetchUserInfo 方法正常执行"); 59 | runInAction(() => { 60 | this.userInfo = rest; 61 | }); 62 | }; 63 | 64 | @action 65 | @track("async.delay", 22122, { delay: 3000 }) 66 | initContent = async val => { 67 | const response = await new Promise(resolve => { 68 | setTimeout(() => { 69 | resolve({ data: "success" }); 70 | }, 300); 71 | }); 72 | console.log("initContent 方法正常执行。并受到参数:", val); 73 | 74 | runInAction(() => { 75 | this.content = response.data; 76 | }); 77 | }; 78 | } 79 | 80 | export default new App(); 81 | -------------------------------------------------------------------------------- /docs/store-mobx/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-13 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Saturday 2019-07-13 13:00:27 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import app from "./app"; 13 | 14 | export default { 15 | app 16 | }; 17 | -------------------------------------------------------------------------------- /docs/store/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-11 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Sunday 2019-07-14 23:07:02 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | export const TYPES = { 13 | GET_USERINFO: "GET_USERINFO" 14 | }; 15 | 16 | const getUserInfo = payload => ({ 17 | type: TYPES.GET_USERINFO, 18 | payload 19 | }); 20 | 21 | export function fetchUserInfo() { 22 | return async dispatch => { 23 | const rest = await new Promise(resolve => { 24 | setTimeout(() => { 25 | resolve({ name: "LHammer", age: "18" }); 26 | }, 100); 27 | }); 28 | 29 | dispatch(getUserInfo(rest)); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /docs/store/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-11 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Thursday 2019-07-11 14:45:15 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import { createStore, applyMiddleware, compose } from "redux"; 13 | import thunk from "redux-thunk"; 14 | import reducer from "./reducer"; 15 | 16 | export default createStore(reducer, compose(applyMiddleware(thunk))); 17 | -------------------------------------------------------------------------------- /docs/store/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-11 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Thursday 2019-07-11 15:45:10 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import { TYPES } from "./actions"; 13 | 14 | export default (state = {}, action) => { 15 | switch (action.type) { 16 | case TYPES.GET_USERINFO: 17 | return Object.assign({}, state, { 18 | userInfo: action.payload 19 | }); 20 | default: 21 | return state; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /docs/styles/animation.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-06-02 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Wednesday 2019-08-14 11:29:14 am 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | */ 10 | @include block("fade-in") { 11 | &-enter-active, 12 | &-leave-active { 13 | opacity: 1; 14 | transition: opacity .3s linear; 15 | } 16 | &-enter, 17 | &-leave, 18 | &-leave-active { 19 | opacity: 0; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/styles/common.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-06-02 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Wednesday 2019-08-14 11:29:08 am 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | */ 10 | /* Font size 公用类 11 | ---------------------------- */ 12 | @include block(ft) { 13 | &\:12 { 14 | font-size: $--font-size-small !important; 15 | } 16 | &\:13 { 17 | font-size: #{$--font-size-base - 1px} !important; 18 | } 19 | &\:14 { 20 | font-size: $--font-size-base !important; 21 | } 22 | &\:15 { 23 | font-size: #{$--font-size-base + 1px} !important; 24 | } 25 | &\:16 { 26 | font-size: $--font-size-medium !important; 27 | } 28 | &\:17 { 29 | font-size: #{$--font-size-base + 3px} !important; 30 | } 31 | &\:18 { 32 | font-size: $--font-size-large !important; 33 | } 34 | &\:20 { 35 | font-size: #{$--font-size-large + 2px} !important; 36 | } 37 | &\:22 { 38 | font-size: #{$--font-size-large + 4px} !important; 39 | } 40 | } 41 | 42 | /* 主题色公用类 43 | ---------------------------- */ 44 | @include block(primary) { 45 | color: $--color-primary !important; 46 | &\:hover { 47 | color: $--color-primary !important; 48 | } 49 | } 50 | 51 | /* 相对定位公用类 52 | ---------------------------- */ 53 | @include global(relative) { 54 | position: relative; 55 | } 56 | 57 | /* 元素全屏公用类 58 | ---------------------------- */ 59 | @include global(overlay) { 60 | @include position--overlay; 61 | } 62 | 63 | /* 顺时针旋转度数公用类 64 | ---------------------------- */ 65 | @include global(rotate) { 66 | &\:180 { 67 | transform: rotate(.5turn); 68 | } 69 | } 70 | 71 | /* touch背景提示公用类 72 | ---------------------------- */ 73 | @include global(touch\:active) { 74 | background: rgba(0, 0, 0, 0.02); 75 | } 76 | -------------------------------------------------------------------------------- /docs/styles/global.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-06-18 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Thursday 2019-08-22 16:49:55 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | */ 10 | @import "./reset/index"; 11 | @import "./common"; 12 | @import "./animation"; 13 | 14 | header { 15 | background: $--color-dark; 16 | padding: 32px; 17 | text-align: center; 18 | .title { 19 | color: $--color-primary; 20 | padding-bottom: 12px; 21 | } 22 | .description { 23 | color: white; 24 | margin-top: 16px; 25 | } 26 | .badge + .badge { 27 | margin-left: 5px; 28 | } 29 | .link { 30 | display: inline-block; 31 | padding: 9px 16px; 32 | color: #fff; 33 | background: lighten($--color-dark, 10%); 34 | border-radius: 3px; 35 | margin-top: 32px; 36 | &:hover { 37 | background: lighten($--color-dark, 20%); 38 | } 39 | &:not(:last-child) { 40 | margin-right: 8px; 41 | } 42 | } 43 | } 44 | 45 | .global-title { 46 | margin: 20px 0 20px 0; 47 | } 48 | 49 | .global-pdtb { 50 | &\:12 { 51 | margin: 12px 0; 52 | } 53 | } 54 | 55 | .footer { 56 | font-size: 14px; 57 | text-align: center; 58 | color: white; 59 | background: lighten($--color-dark, 45%); 60 | &-content { 61 | margin: 0 42px; 62 | padding: 16px 0; 63 | } 64 | } 65 | 66 | .command { 67 | background: darken($--color-dark, 10%); 68 | color: white; 69 | font-family: monospace; 70 | max-width: 500px; 71 | margin: 20px auto; 72 | border-radius: 2px; 73 | padding: 12px 24px; 74 | box-sizing: border-box; 75 | text-align: center; 76 | } 77 | 78 | .r-track { 79 | max-width: 1200px; 80 | margin: 0 auto; 81 | padding-bottom: 20px; 82 | .ant-btn { 83 | display: block; 84 | width: 229px; 85 | margin: 32px auto 52px; 86 | } 87 | .description { 88 | padding: 20px 0; 89 | } 90 | } 91 | 92 | .started_page { 93 | max-width: 1200px; 94 | padding: 20px; 95 | margin: auto; 96 | } 97 | 98 | .start_alert { 99 | margin-top: 20px; 100 | } 101 | 102 | .code-snippet { 103 | @include h-box; 104 | background: #F6F6F6; 105 | border-radius: 3px; 106 | font-family: "Roboto Mono", monospace; 107 | font-size: 10pt; 108 | overflow: auto; 109 | position: relative; 110 | .line-numbers, 111 | .render { 112 | padding: 20px; 113 | padding-right: 0; 114 | } 115 | .line-numbers { 116 | border-radius: 2px 0 0 2px; 117 | line-height: 18px; 118 | } 119 | .render { 120 | white-space: pre; 121 | line-height: 18px; 122 | } 123 | .language { 124 | position: absolute; 125 | top: 0; 126 | right: 0; 127 | padding: 4px 4px 6px 6px; 128 | border-radius: 0 0 0 2px; 129 | } 130 | } 131 | 132 | .r-track-table { 133 | font-size: 12px; 134 | .header, 135 | .row { 136 | display: flex; 137 | border-bottom: 1px solid #ebeef5; 138 | strong, 139 | span { 140 | line-height: 29px; 141 | } 142 | .key { 143 | width: 92px; 144 | } 145 | .val { 146 | flex: 1; 147 | } 148 | } 149 | .row:hover { 150 | background: #fafafa; 151 | cursor: pointer; 152 | } 153 | } 154 | 155 | .block_show__card { 156 | width: 399px; 157 | text-align: center; 158 | margin: 32px auto 52px; 159 | } -------------------------------------------------------------------------------- /docs/styles/mixins.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-06-02 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Monday 2019-08-19 12:17:56 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | */ 10 | $M_E: 'null'; 11 | 12 | @mixin block($block) { 13 | $B: $block !global; 14 | .#{$block} { 15 | $rootblock: & !global; 16 | @content; 17 | } 18 | } 19 | 20 | @mixin e($element, $base: 'root') { 21 | $E: $element !global; 22 | @if $M_E != 'null' { 23 | @if $M_E == $rootblock { 24 | & .#{$element} { 25 | @content; 26 | } 27 | } @else { 28 | & #{$M_E}__#{$element} { 29 | $M_E: 'null' !global; // 清空当前修饰的元素 30 | @content; 31 | } 32 | } 33 | } @else if $base != 'root' { 34 | @at-root .#{$base}__#{$element} { 35 | @content; 36 | } 37 | } @else if & == $rootblock { 38 | & .#{$element} { 39 | @content; 40 | } 41 | } @else if $base == 'root' { 42 | &__#{$element} { 43 | @content; 44 | } 45 | } 46 | } 47 | 48 | @mixin m($modifier, $nexted: 'false') { 49 | @if $nexted == 'true' { 50 | $M_E: & !global; 51 | } 52 | &--#{$modifier} { 53 | @content; 54 | } 55 | } 56 | 57 | @mixin global($name) { 58 | $cls: global-#{$name}; 59 | @at-root .#{$cls} { 60 | @content; 61 | } 62 | } 63 | 64 | @mixin icon($w, $h, $bg, $type: 'png') { 65 | width: #{$w}px; 66 | height: #{$h}px; 67 | background: none no-repeat center / 100%; 68 | @if $type == 'svg' { 69 | background-image: url('#{$bg}.svg'); 70 | } @else { 71 | background-image: url('#{$bg}@2x.png'); 72 | @media (min-resolution: 3dppx) { 73 | background-image: url('#{$bg}@3x.png'); 74 | } 75 | } 76 | } 77 | 78 | @mixin one-pix-line($pos: 'bottom', $color: $--border-color, $style: solid) { 79 | $prop: border-#{$pos}; 80 | @if $pos == 'all' { 81 | $prop: border; 82 | } 83 | #{$prop}: 1px $style $color; 84 | @media (min-resolution: 2dppx) { 85 | #{$prop}: .5px $style $color; 86 | } 87 | @media (min-resolution: 3dppx) { 88 | #{$prop}: .333333px $style $color; 89 | } 90 | @media (min-resolution: 4dppx) { 91 | #{$prop}: .25px $style $color; 92 | } 93 | } 94 | 95 | @mixin element-a { 96 | display: inline-block; 97 | outline: none; 98 | line-height: $--font-line-height-base; 99 | text-decoration: none; 100 | color: $--color-text; 101 | cursor: pointer; 102 | &:hover, 103 | &:focus { 104 | color: $--color-primary; 105 | } 106 | } 107 | 108 | @mixin text-ellipsis($width: 100%) { 109 | width: $width; 110 | overflow: hidden; 111 | white-space: nowrap; 112 | word-wrap: normal; 113 | text-overflow: ellipsis; 114 | } 115 | 116 | @mixin text-wrap() { 117 | white-space: pre-wrap; 118 | word-wrap: break-word; 119 | } 120 | 121 | @mixin position--t--r($p: absolute, $t: 0, $r: 0) { 122 | position: $p; 123 | top: $t; 124 | right: $r; 125 | @content; 126 | } 127 | 128 | @mixin position--t--b($p: absolute, $t: 0, $b: 0) { 129 | position: $p; 130 | top: $t; 131 | bottom: $b; 132 | @content; 133 | } 134 | 135 | @mixin position--t--l($p: absolute, $t: 0, $l: 0) { 136 | position: $p; 137 | top: $t; 138 | left: $l; 139 | @content; 140 | } 141 | 142 | @mixin position--b--r($p: absolute, $b: 0, $r: 0) { 143 | position: $p; 144 | right: $r; 145 | bottom: $b; 146 | @content; 147 | } 148 | 149 | @mixin position--b--l($p: absolute, $b: 0, $l: 0) { 150 | position: $p; 151 | bottom: $b; 152 | left: $l; 153 | @content; 154 | } 155 | 156 | @mixin position--r--l($p: absolute, $r: 0, $l: 0) { 157 | position: $p; 158 | right: $r; 159 | left: $l; 160 | @content; 161 | } 162 | 163 | @mixin position--overlay($p: absolute, $v: 0) { 164 | position: $p; 165 | top: $v; 166 | right: $v; 167 | bottom: $v; 168 | left: $v; 169 | } 170 | 171 | @mixin position--overlay--y($p: absolute) { 172 | position: $p; 173 | top: 0; 174 | bottom: 0; 175 | } 176 | 177 | @mixin center--x() { 178 | position: absolute; 179 | left: 50%; 180 | transform: translateX(-50%); 181 | @content; 182 | } 183 | 184 | @mixin center--y() { 185 | position: absolute; 186 | top: 50%; 187 | transform: translateY(-50%); 188 | @content; 189 | } 190 | 191 | @mixin flex-box { 192 | display: flex; 193 | & > * { 194 | flex: 0 0 auto; 195 | } 196 | } 197 | 198 | @mixin h-box { 199 | @include flex-box; 200 | flex-direction: row; 201 | @content; 202 | } 203 | 204 | @mixin v-box { 205 | @include flex-box; 206 | flex-direction: column; 207 | @content; 208 | } 209 | 210 | @mixin box-center { 211 | @include flex-box; 212 | align-items: center; 213 | justify-content: center; 214 | @content; 215 | } 216 | 217 | @mixin box-between { 218 | @include flex-box; 219 | align-items: center; 220 | justify-content: space-between; 221 | @content; 222 | } 223 | 224 | @mixin circle($w, $color) { 225 | width: $w; 226 | height: $w; 227 | border-radius: 50%; 228 | background: $color; 229 | } -------------------------------------------------------------------------------- /docs/styles/reset/index.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-06-02 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Monday 2019-08-19 12:18:01 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | */ 10 | @import "./normalize.scss"; 11 | 12 | html { 13 | box-sizing: border-box; 14 | font-family: "PingFang SC", "Helvetica Neue", Helvetica, "Hiragino Sans GB", 15 | "Microsoft YaHei", "微软雅黑", Arial, sans-serif; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | color: $--color-text; 19 | font-size: $--font-size-base; 20 | line-height: 1; 21 | #app > * { 22 | min-height: 100vh; 23 | } 24 | } 25 | 26 | *:not(input) { 27 | border: 0; 28 | margin: 0; 29 | padding: 0; 30 | } 31 | 32 | *, 33 | *::before, 34 | *::after { 35 | box-sizing: inherit; 36 | } 37 | 38 | ul, 39 | li { 40 | list-style: none 41 | } 42 | 43 | a { 44 | text-decoration: none; 45 | } 46 | 47 | input { 48 | outline: none; 49 | -webkit-appearance: none; 50 | &::-moz-placeholder, 51 | &::-ms-input-placeholder, 52 | &::-webkit-input-placeholder { 53 | color: $--color-text-placeholder; 54 | } 55 | &[type="search"]::-webkit-search-cancel-button { 56 | display: none; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /docs/styles/reset/normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } 350 | -------------------------------------------------------------------------------- /docs/styles/variables.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-06-02 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Monday 2019-08-19 17:18:15 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | */ 10 | $namespace: "hp"; 11 | 12 | /* Color 13 | ---------------------------- */ 14 | $--color-primary: #61dafb !default; 15 | $--color-lightprimary: rgba($--color-primary, .2) !default; 16 | $--color-dark: #282c34 !default; 17 | $--color-darker: #20232a !default; 18 | $--color-gray: #CCCCCC !default; 19 | $--color-lightgray: #F6F6F6 !default; 20 | $--color-white: #FFFFFF !default; 21 | $--color-text: #333333 !default; 22 | $--color-text-gray: #666666 !default; 23 | $--color-text-lightgray: #9399A5 !default; 24 | $--color-text-lightergray: #B8B8B8 !default; 25 | $--color-text-placeholder: #999999 !default; 26 | $--color-text-disabled: #BFBFBF !default; 27 | 28 | /* Font 29 | ---------------------------- */ 30 | $--font-size-small: 12px !default; 31 | $--font-size-base: 14px !default; 32 | $--font-size-medium: 16px !default; 33 | $--font-size-large: 18px !default; 34 | $--font-line-height-base: 18px !default; 35 | 36 | /* Border 37 | ---------------------------- */ 38 | $--border-color: #E4E6F0 !default; 39 | $--border-color-gray: $--color-gray !default; 40 | $--border-radius: 4px; 41 | 42 | /* Box-shadow 43 | ---------------------------- */ 44 | $--box-shadow-base: 0 2px 8px 0 rgba(0, 0, 0, .12) !default; 45 | $--box-shadow-dark: 0 0px 8px 0 rgba(0, 0, 0, .15) !default; 46 | 47 | /* Border-radius 48 | ---------------------------- */ 49 | $--border-radius-base: 4px !default; 50 | 51 | /* Badge 52 | ---------------------------- */ 53 | $--badge-color: #FF4949; 54 | 55 | $md-grey: #9e9e9e; 56 | $md-grey-100: #f5f5f5; 57 | $md-grey-200: #eeeeee; 58 | $md-grey-300: #e0e0e0; 59 | $md-grey-400: #bdbdbd; 60 | -------------------------------------------------------------------------------- /docs/tracks/action.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-06-19 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Wednesday 2019-08-21 21:20:50 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import React from "react"; 13 | import { notification } from "antd"; 14 | import { format } from "@/utils/date"; 15 | import $log from "@/utils/logger"; 16 | import Table from "@/components/table"; 17 | 18 | /** 19 | * 埋点Action 20 | * 21 | * @description 埋点参数包括 基本参数 和 行为参数 22 | * @param {Object} action 行为参数 23 | * @param {Object} other 基本参数 一般不需要传此参数 24 | */ 25 | /** 26 | * @desc 模拟埋点Action 27 | */ 28 | export default function trackAction(evt, addtional = {}) { 29 | const data = { 30 | evt, 31 | ...addtional, 32 | action_time: format(Date.now()) 33 | }; 34 | 35 | $log(data); 36 | notification.success({ 37 | message: "上报数据如下:", 38 | description:
39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /docs/tracks/events/home.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-06-19 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Wednesday 2019-08-21 21:28:11 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import trackAction from "../action"; 13 | 14 | export default { 15 | /** 16 | * UP、PV埋点 17 | */ 18 | UVPV() { 19 | trackAction("1,3"); 20 | }, 21 | /** 22 | * 页面停留时间埋点(Time on Page) 23 | * @param {String} stt 进入页面时长,单位为毫秒 24 | */ 25 | TONP(_, { stt }) { 26 | trackAction("2", { stt }); 27 | }, 28 | 22120({ userInfo }) { 29 | trackAction("22120", { 30 | userInfo: JSON.stringify(userInfo) 31 | }); 32 | }, 33 | 22121(context, val) { 34 | trackAction("22121", { 35 | tamp: context.date, 36 | param: val 37 | }); 38 | }, 39 | 22122(context, val) { 40 | trackAction("22122", { 41 | content: context.content, 42 | param: val 43 | }); 44 | }, 45 | 22123(_, val) { 46 | trackAction("22123", { 47 | param: val 48 | }); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /docs/tracks/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-06-19 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Monday 2019-07-15 14:56:34 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | // const eventModules = require.context("./events", false, /\.js/); 13 | 14 | // export default eventModules.keys().reduce((events, module) => { 15 | // const prop = /[^(./)?][\w|\W]*[^(.js)?]/.exec(module)[0]; 16 | 17 | // events[prop] = eventModules(module).default; 18 | // return events; 19 | // }, {}); 20 | 21 | export { default as home } from "./events/home"; 22 | -------------------------------------------------------------------------------- /docs/utils/date.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-08 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Monday 2019-07-08 22:46:36 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | /** 13 | * @author LHammer 14 | * @desc date parse or format date 15 | * @see https://github.com/l-hammer/YDTemplate/blob/master/src/utils/es6/date.js 16 | */ 17 | const twoDigits = /\d\d?/; 18 | const fourDigits = /\d{4}/; 19 | const token = /d{1,2}|M{1,2}|yy(?:yy)?|([HhMsDm])\1?|[aA]|"[^"]*"|'[^']*'/g; 20 | const masks = { 21 | default: "yyyy-MM-dd HH:mm:ss", 22 | date: "yyyy-MM-dd", 23 | datetime: "yyyy-MM-dd HH:mm:ss", 24 | time: "HH:mm:ss", 25 | year: "yyyy", 26 | enDate: "M/d/yy", 27 | cnDate: "yyyy 年 MM 月 dd 日" 28 | }; 29 | 30 | const pad = (val, len) => { 31 | val = String(val); 32 | len = len || 2; 33 | while (val.length < len) { 34 | val = `0${val}`; 35 | } 36 | return val; 37 | }; 38 | 39 | const formatFlags = { 40 | yyyy(dateObj) { 41 | return pad(dateObj.getFullYear(), 4); 42 | }, 43 | yy(dateObj) { 44 | return String(dateObj.getFullYear()).substr(2); 45 | }, 46 | M(dateObj) { 47 | return dateObj.getMonth() + 1; 48 | }, 49 | MM(dateObj) { 50 | return pad(dateObj.getMonth() + 1); 51 | }, 52 | d(dateObj) { 53 | return dateObj.getDate(); 54 | }, 55 | dd(dateObj) { 56 | return pad(dateObj.getDate()); 57 | }, 58 | h(dateObj) { 59 | return dateObj.getHours() % 12 || 12; 60 | }, 61 | hh(dateObj) { 62 | return pad(dateObj.getHours() % 12 || 12); 63 | }, 64 | H(dateObj) { 65 | return dateObj.getHours(); 66 | }, 67 | HH(dateObj) { 68 | return pad(dateObj.getHours()); 69 | }, 70 | m(dateObj) { 71 | return dateObj.getMinutes(); 72 | }, 73 | mm(dateObj) { 74 | return pad(dateObj.getMinutes()); 75 | }, 76 | s(dateObj) { 77 | return dateObj.getSeconds(); 78 | }, 79 | ss(dateObj) { 80 | return pad(dateObj.getSeconds()); 81 | } 82 | }; 83 | 84 | /** 85 | * Format a date 86 | * @method format 87 | * @param {Date|number} dateObj new Date(2018, 2, 9) 88 | * @param {String} mask Format of the date e.g. 'yyyy-MM-dd HH:mm:ss' or 'cnDate' 89 | */ 90 | export const format = (dateObj, mask) => { 91 | if (typeof dateObj === "number") { 92 | dateObj = new Date(dateObj); 93 | } 94 | 95 | if ( 96 | Object.prototype.toString.call(dateObj) !== "[object Date]" || 97 | isNaN(dateObj.getTime()) 98 | ) { 99 | throw new Error("Invalid Date in date.format"); 100 | } 101 | mask = masks[mask] || mask || masks.default; 102 | 103 | // return 不可省略 104 | mask = mask.replace(token, $0 => { 105 | return $0 in formatFlags 106 | ? formatFlags[$0](dateObj) 107 | : $0.slice(1, $0.length - 1); 108 | }); 109 | 110 | return mask; 111 | }; 112 | 113 | const parseFlags = { 114 | yyyy: [ 115 | fourDigits, 116 | (d, v) => { 117 | d.year = v; 118 | } 119 | ], 120 | yy: [ 121 | twoDigits, 122 | (d, v) => { 123 | const da = new Date(); 124 | const cent = +`${da.getFullYear()}`.substr(0, 2); 125 | d.year = `${v > 68 ? cent - 1 : cent}${v}`; 126 | } 127 | ], 128 | M: [ 129 | twoDigits, 130 | (d, v) => { 131 | d.month = v - 1; 132 | } 133 | ], 134 | d: [ 135 | twoDigits, 136 | (d, v) => { 137 | d.day = v; 138 | } 139 | ], 140 | h: [ 141 | twoDigits, 142 | (d, v) => { 143 | d.hour = v; 144 | } 145 | ], 146 | m: [ 147 | twoDigits, 148 | (d, v) => { 149 | d.minute = v; 150 | } 151 | ], 152 | s: [ 153 | twoDigits, 154 | (d, v) => { 155 | d.second = v; 156 | } 157 | ] 158 | }; 159 | parseFlags.MM = parseFlags.M; 160 | parseFlags.dd = parseFlags.d; 161 | parseFlags.hh = parseFlags.h; 162 | parseFlags.H = parseFlags.h; 163 | parseFlags.HH = parseFlags.h; 164 | parseFlags.mm = parseFlags.m; 165 | parseFlags.ss = parseFlags.s; 166 | 167 | /** 168 | * Format a date 169 | * @method parse 170 | * @param {String} dateStr Date String e.g. '2018-02-09 09:29:29' or '2018 年 02 月 09 日' 171 | * @param {String} mask Parse of the format date e.g. 'yyyy-MM-dd HH:mm:ss' or 'cnDate' 172 | * @param {Date} 173 | */ 174 | export const parse = (dateStr, mask) => { 175 | let isVaild = true; 176 | const dateInfo = {}; 177 | const today = new Date(); 178 | 179 | if (typeof dateStr !== "string") { 180 | throw new Error("Invalid format in fecha.parse"); 181 | } 182 | 183 | mask = masks[mask] || mask || masks.default; 184 | /** 185 | * @function replace @see https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace; 186 | * @param {String} $0 匹配的子串 187 | */ 188 | mask.replace(token, function($0) { 189 | if (parseFlags[$0]) { 190 | const flag = parseFlags[$0]; 191 | /** 192 | * 搜索匹配到子串(e.g. yyyy)对应flag(fourDigits)的位置 193 | * @function search 未匹配到时返回-1,即按位取反为0时表示没有对应的flag 194 | */ 195 | const index = dateStr.search(flag[0]); 196 | if (!~index) { 197 | isVaild = false; 198 | } else { 199 | /** 200 | * 为避免重复返回,将已经返回的值result从dateStr中删除 201 | */ 202 | dateStr.replace(flag[0], function(result) { 203 | flag[1](dateInfo, result); 204 | dateStr = dateStr.substr(index + result.length); 205 | return result; 206 | }); 207 | } 208 | } 209 | return parseFlags[$0] ? "" : $0.slice(1, $0.length - 1); 210 | }); 211 | 212 | if (!isVaild) { 213 | return false; 214 | } 215 | 216 | const date = new Date( 217 | dateInfo.year || today.getFullYear(), 218 | dateInfo.month || 0, 219 | dateInfo.day || 1, 220 | dateInfo.hour || 0, 221 | dateInfo.minute || 0, 222 | dateInfo.second || 0 223 | ); 224 | return date; 225 | }; 226 | 227 | export default { 228 | format, 229 | parse 230 | }; 231 | -------------------------------------------------------------------------------- /docs/utils/logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-08 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Wednesday 2019-08-21 21:39:38 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import { format } from "./date"; 13 | 14 | const logger = console; 15 | const message = () => format(Date.now()); 16 | 17 | const $log = (...rest) => { 18 | logger.log( 19 | `%c ${message()} r-track`, 20 | "color: #03A9F4; font-weight: bold", 21 | ...rest 22 | ); 23 | }; 24 | 25 | $log.error = rest => { 26 | logger.log( 27 | `%c ${message()} r-track`, 28 | "color: #F56C6C; font-weight: bold", 29 | rest 30 | ); 31 | return Promise.reject(rest); 32 | }; 33 | 34 | export default $log; 35 | -------------------------------------------------------------------------------- /docs/views/block-show.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-08-22 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Friday 2019-08-23 16:03:48 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | */ 10 | /* 11 | * Created Date: 2019-08-22 12 | * Author: 宋慧武 13 | * ------ 14 | * Last Modified: Thursday 2019-08-22 13:57:18 pm 15 | * Modified By: the developer formerly known as 宋慧武 at 16 | * ------ 17 | * HISTORY: 18 | */ 19 | import React, { Component } from "react"; 20 | import { Provider } from "react-redux"; 21 | import { withRouter } from "react-router"; 22 | import { Card, Alert } from "antd"; 23 | import CodeSnippet from "@/components/code-snippet"; 24 | import store from "@/store"; 25 | import { home as trackEvents } from "@/tracks"; 26 | import $log from "@/utils/logger"; 27 | import { track, inject } from "../../"; 28 | 29 | const blockShowSnippet = ` 30 | @inject({ trackEvents }) 31 | class BlockShow extends Component { 32 | constructor(props) { 33 | super(props); 34 | this.cardTrackRef = null; 35 | } 36 | 37 | @track("show") 38 | render() { 39 | return ( 40 | (this.cardTrackRef = ref)} 44 | className="block_show__card" 45 | > 46 |

我想被曝光无数次

47 |
48 | ); 49 | } 50 | } 51 | `; 52 | 53 | const blockShowOnceSnippet = ` 54 | @inject({ trackEvents }) 55 | class BlockShow extends Component { 56 | constructor(props) { 57 | super(props); 58 | this.cardTrackRef = null; 59 | } 60 | 61 | @track("show.once") 62 | render() { 63 | return ( 64 | (this.cardTrackRef = ref)} 68 | className="block_show__card" 69 | > 70 |

我只想被曝光一次

71 |
72 | ); 73 | } 74 | } 75 | `; 76 | 77 | @withRouter 78 | @inject({ trackEvents }) 79 | class BlockShow extends Component { 80 | constructor(props) { 81 | super(props); 82 | this.firstCardTrackRef = null; 83 | this.secondCardTrackRef = null; 84 | } 85 | 86 | componentDidMount() { 87 | $log("页面挂载完成"); 88 | } 89 | 90 | @track("show") 91 | render() { 92 | return ( 93 |
94 |

95 | 如果我们需要统计某个元素的曝光量,可增加修饰符show,这里以 Antd Card 96 | 组件为例,代码如下: 97 |

98 | (this.firstCardTrackRef = ref)} 102 | className="block_show__card" 103 | > 104 |

我想被曝光无数次

105 |
106 | 107 |

108 | 如果我们只需要统计一次曝光,可增加once修饰符,代码如下: 109 |

110 | (this.secondCardTrackRef = ref)} 115 | className="block_show__card" 116 | > 117 |

我只想被曝光一次

118 |
119 | 120 | 124 |
125 | ); 126 | } 127 | } 128 | 129 | export default class WithBlockShow extends Component { 130 | render() { 131 | return ( 132 | 133 | 134 | 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /docs/views/home-mobx/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-14 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Thursday 2019-08-22 13:59:43 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import React, { Component, Fragment } from "react"; 13 | import { Provider, inject, observer } from "mobx-react"; 14 | import { withRouter } from "react-router"; 15 | import store from "@/store-mobx"; 16 | import { home as trackEvents } from "@/tracks"; 17 | import { track } from "../../../"; 18 | 19 | @withRouter 20 | @inject(({ app }) => ({ 21 | app, 22 | trackEvents 23 | })) 24 | @observer 25 | class Detail extends Component { 26 | constructor(props) { 27 | super(props); 28 | this.buttonTrackRef = null; 29 | } 30 | 31 | @track("UVPV") 32 | @track("TONP") 33 | componentDidMount() { 34 | this.props.app.initContent("test"); 35 | this.props.app.fetchUserInfo(); 36 | } 37 | 38 | @track("show") 39 | render() { 40 | const { handleClick, date } = this.props.app; 41 | 42 | return ( 43 | 44 |
45 | 53 |

tmsp: {date}

54 |
55 | ); 56 | } 57 | } 58 | 59 | export default class WrapDetail extends Component { 60 | render() { 61 | return ( 62 | 63 | 64 | 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /docs/views/home-redux/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-14 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Thursday 2019-08-22 16:49:55 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import React, { Component } from "react"; 13 | import { Provider, connect } from "react-redux"; 14 | import { Button } from "antd"; 15 | import { withRouter } from "react-router"; 16 | import store from "@/store"; 17 | import { home as trackEvents } from "@/tracks"; 18 | import { fetchUserInfo } from "@/store/actions"; 19 | import { track, inject } from "../../../"; 20 | 21 | function mapStateToProps(state) { 22 | return { 23 | userInfo: state.userInfo 24 | }; 25 | } 26 | 27 | @withRouter 28 | @connect(mapStateToProps) 29 | @inject({ trackEvents }) 30 | class Home extends Component { 31 | constructor(props) { 32 | super(props); 33 | this.buttonRef = null; 34 | this.state = { 35 | date: null, 36 | target: null, 37 | content: null 38 | }; 39 | } 40 | 41 | @track("trigger", 22121) 42 | handleClick(val, e) { 43 | e.persist(); 44 | console.log("handleClick 方法正常执行。并受到参数:", val, e.target); 45 | this.setState({ 46 | date: Date.now(), 47 | target: e.target 48 | }); 49 | } 50 | 51 | // @track("async.once", 22121) 52 | // handleClick = async (val, e) => { 53 | // e.persist(); 54 | // const response = await new Promise(resolve => { 55 | // setTimeout(() => { 56 | // resolve({ date: Date.now() }); 57 | // }, 300); 58 | // }); 59 | // console.log( 60 | // "handleClick 方法正常执行。并受到参数:", 61 | // val, 62 | // e.target, 63 | // response.date 64 | // ); 65 | 66 | // this.setState({ 67 | // date: response.date, 68 | // target: e.target 69 | // }); 70 | // }; 71 | 72 | @track("UVPV") 73 | @track("TONP") 74 | componentDidMount = () => { 75 | this.initContent("test"); 76 | this.getUserInfo(); 77 | 78 | this.timer = setTimeout(() => { 79 | console.log("修改 stateKey: content 方法正常执行"); 80 | this.setState({ 81 | content: { test: "test" } 82 | }); 83 | }, 4000); 84 | }; 85 | 86 | componentWillUnmount() { 87 | this.timer && clearTimeout(this.timer); 88 | } 89 | 90 | @track("async", 22120) 91 | getUserInfo = async () => { 92 | console.log("getUserInfo 方法正常执行"); 93 | await this.props.dispatch(fetchUserInfo()); 94 | }; 95 | 96 | @track("async.delay", 22122, { delay: 3000 }) 97 | initContent = async val => { 98 | const response = await new Promise(resolve => { 99 | setTimeout(() => { 100 | resolve({ data: "success" }); 101 | }, 300); 102 | }); 103 | console.log("initContent 方法正常执行。并受到参数:", val); 104 | this.setState({ 105 | content: response.data 106 | }); 107 | }; 108 | 109 | render() { 110 | return ( 111 | // 117 |
118 | 125 |
126 | ); 127 | } 128 | } 129 | 130 | export default class WrapHome extends Component { 131 | render() { 132 | return ( 133 | 134 | 135 | 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /docs/views/page-init.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-08-22 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Thursday 2019-08-22 16:49:55 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | */ 10 | /* 11 | * Created Date: 2019-08-21 12 | * Author: 宋慧武 13 | * ------ 14 | * Last Modified: Wednesday 2019-08-21 22:17:01 pm 15 | * Modified By: the developer formerly known as 宋慧武 at 16 | * ------ 17 | * HISTORY: 18 | * ------ 19 | * Javascript will save your soul! 20 | */ 21 | import React, { Component } from "react"; 22 | import { Provider, connect } from "react-redux"; 23 | import { withRouter } from "react-router"; 24 | import { message, Alert } from "antd"; 25 | import CodeSnippet from "@/components/code-snippet"; 26 | import store from "@/store"; 27 | import { home as trackEvents } from "@/tracks"; 28 | import $log from "@/utils/logger"; 29 | import { track, inject } from "../../"; 30 | 31 | const UVPVTONPSnippet = ` 32 | @inject({ trackEvents }) 33 | class Home extends Component { 34 | ... 35 | @track("UVPV") 36 | @track("TONP") 37 | componentDidMount() { 38 | ... 39 | } 40 | ... 41 | } 42 | `; 43 | 44 | const ASYNCDELAYSnippet = ` 45 | @inject({ trackEvents }) 46 | class Home extends Component { 47 | state = { 48 | content: null 49 | }; 50 | 51 | @track("async.delay", 22122, { delay: 3000 }) 52 | initContent = async val => { 53 | const response = await new Promise(resolve => { 54 | setTimeout(() => { 55 | resolve({ data: "success" }); 56 | }, 300); 57 | }); 58 | 59 | this.setState({ 60 | content: response.data 61 | }); 62 | }; 63 | ... 64 | } 65 | `; 66 | 67 | function mapStateToProps(state) { 68 | return { 69 | userInfo: state.userInfo 70 | }; 71 | } 72 | 73 | @withRouter 74 | @connect(mapStateToProps) 75 | @inject({ trackEvents }) 76 | class Trigger extends Component { 77 | state = { 78 | content: null 79 | }; 80 | 81 | @track("UVPV") 82 | @track("TONP") 83 | componentDidMount() { 84 | message.success("页面挂载完成,发送UPPV埋点"); 85 | this.initContent("一个页面延迟3s埋点"); 86 | } 87 | 88 | @track("async.delay", 22122, { delay: 3000 }) 89 | initContent = async val => { 90 | const response = await new Promise(resolve => { 91 | setTimeout(() => { 92 | resolve({ data: "success" }); 93 | }, 300); 94 | }); 95 | 96 | $log("initContent 方法正常执行。并受到参数:", val); 97 | this.setState({ 98 | content: response.data 99 | }); 100 | }; 101 | 102 | componentWillUnmount() { 103 | message.success("页面即将销毁,发送TONP埋点"); 104 | } 105 | 106 | render() { 107 | return ( 108 |
109 |

110 | 如果我们需要统计当前模块(页面)的UV、PV、TONP(页面停留时长)埋点,示例如下: 111 |

112 | 113 | 117 |

118 | 如果我们需要在页面初始化3s之后上报埋点,可增加delay修饰符,示例如下: 119 |

120 | 121 | 125 |
126 | ); 127 | } 128 | } 129 | 130 | export default class WithTrigger extends Component { 131 | render() { 132 | return ( 133 | 134 | 135 | 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /docs/views/started.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-08-19 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Thursday 2019-08-22 13:54:59 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import React, { Component, Fragment } from "react"; 13 | import CodeSnippet from "@/components/code-snippet"; 14 | import { Alert } from "antd"; 15 | 16 | const installSnippet = ` 17 | # YARN 18 | $ yarn add r-track 19 | 20 | # NPM 21 | $ npm install r-track --save 22 | `; 23 | 24 | const injectUsageSnippet = ` 25 | // react class component 26 | import { inject } from "r-track"; 27 | import tracks from "@/tracks"; 28 | @inject({ trackEvents: tracks.home }) 29 | 30 | // mobx class 31 | import { inject } from "r-track"; 32 | import tracks from "@/tracks"; 33 | @inject({ trackEvents: tracks.home }) 34 | 35 | // react class component + mobx-react 36 | import { inject } from "mobx-react"; 37 | import tracks from "@/tracks"; 38 | @inject(store => ({ store, trackEvents: tracks.home })) 39 | `; 40 | 41 | const trackUsageSnippet = ` 42 | import { track } from "r-track"; 43 | 44 | @track("UVPV") // 访问量埋点 45 | @track("TONP") // 页面停留时长埋点 46 | @track("trigger", 22121) // 同步事件行为埋点 47 | @track("trigger.after", 22121) // 同上,方法执行完成后触发 48 | @track("trigger.once", 22121) // 同上,只触发一次 49 | @track("trigger.after.once", 22121) // 只触发一次且在方法执行完成后触发 50 | @track("async", 22121) // 异步行为埋点 51 | @track("async.once", 22121) // 同上,只触发一次 52 | @track("async.delay", 22121, { delay: 3000, ref: "elementRef") // 异步延时行为埋点 53 | `; 54 | 55 | export default class Started extends Component { 56 | constructor(props) { 57 | super(props); 58 | this.state = {}; 59 | } 60 | 61 | tips = () => { 62 | return ( 63 | 64 |

65 | 1、ref 66 | 参数为DOM的引用,可控制在小于埋点延迟时间内发生DOM隐藏是否继续上报埋点; 67 |

68 |

2、delay 参数为埋点延迟上报的时间;

69 |

70 | 3、async.delay 类型的埋点暂不支持 once 71 | 修饰符,因为目前还没有适用的场景。 72 |

73 |
74 | ); 75 | }; 76 | 77 | render() { 78 | return ( 79 |
80 |
81 | r-track是一个基于装饰器的埋点业务插件,同样可实现将埋点代码与业务代码完全解耦,适用于react项目。完整示例可参考 82 | 83 | GitHub 84 | 85 |
86 |

安装

87 | 88 |

用法

89 |

注入当前模块(页面)相关的埋点声明~

90 | 91 |

埋点装饰器,对应 v-track 中的指令~

92 | 93 | 99 |
100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /docs/views/trigger.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-08-21 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Thursday 2019-08-22 16:49:55 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import React, { Component } from "react"; 13 | import { Provider, connect } from "react-redux"; 14 | import { withRouter } from "react-router"; 15 | import { Button, message, Alert } from "antd"; 16 | import CodeSnippet from "@/components/code-snippet"; 17 | import store from "@/store"; 18 | import { home as trackEvents } from "@/tracks"; 19 | import $log from "@/utils/logger"; 20 | import { track, inject } from "../../"; 21 | 22 | const triggerSnippet = ` 23 | @inject({ trackEvents }) 24 | class Home extends Component { 25 | state = { 26 | date: Date.now(), 27 | }; 28 | 29 | @track("trigger", 22121) 30 | handleClick(val, e) { 31 | this.setState({ date: Date.now() }); 32 | } 33 | 34 | render() { 35 | return ( 36 | 37 | ); 38 | } 39 | } 40 | `; 41 | 42 | const triggerAfterSnippet = ` 43 | @inject({ trackEvents }) 44 | class Home extends Component { 45 | state = { 46 | date: Date.now(), 47 | }; 48 | 49 | @track("trigger.after", 22121) 50 | handleClick(val, e) { 51 | this.setState({ date: Date.now() }); 52 | } 53 | 54 | render() { 55 | return ( 56 | 57 | ); 58 | } 59 | } 60 | `; 61 | 62 | const triggerAsyncSnippet = ` 63 | @inject({ trackEvents }) 64 | class Home extends Component { 65 | state = { 66 | date: Date.now(), 67 | }; 68 | 69 | @track("async", 22121) 70 | handleClick(val, e) { 71 | const response = await new Promise(resolve => { 72 | setTimeout(() => { 73 | resolve({ date: Date.now() }); 74 | }, 300); 75 | }); 76 | 77 | this.setState({ date: response.date }); 78 | } 79 | 80 | render() { 81 | return ( 82 | 83 | ); 84 | } 85 | } 86 | `; 87 | 88 | const triggerAsyncActionSnippet = ` 89 | @inject({ trackEvents }) 90 | class Home extends Component { 91 | ... 92 | @track("async", 22120) 93 | getUserInfo = async () => { 94 | await this.props.dispatch(fetchUserInfo()); 95 | }; 96 | ... 97 | } 98 | `; 99 | 100 | const triggerAsyncOnceSnippet = ` 101 | @inject({ trackEvents }) 102 | class Home extends Component { 103 | state = { 104 | date: Date.now(), 105 | }; 106 | 107 | @track("async.once", 22121) 108 | handleClick(val, e) { 109 | const response = await new Promise(resolve => { 110 | setTimeout(() => { 111 | resolve({ date: Date.now() }); 112 | }, 300); 113 | }); 114 | 115 | this.setState({ date: response.date }); 116 | } 117 | 118 | render() { 119 | return ( 120 | 121 | ); 122 | } 123 | } 124 | `; 125 | 126 | function mapStateToProps(state) { 127 | return { 128 | userInfo: state.userInfo 129 | }; 130 | } 131 | 132 | @withRouter 133 | @connect(mapStateToProps) 134 | @inject({ trackEvents }) 135 | class Trigger extends Component { 136 | constructor(props) { 137 | super(props); 138 | this.buttonRef = null; 139 | this.state = { 140 | date: null, 141 | target: null, 142 | content: null 143 | }; 144 | } 145 | 146 | @track("trigger", 22121) 147 | handleClick(val, e) { 148 | $log("handleClick 方法正常执行。并收到参数:", val, e.target); 149 | message.success("handleClick 方法正常执行"); 150 | this.setState({ 151 | date: Date.now(), 152 | target: e.target 153 | }); 154 | } 155 | 156 | @track("trigger.after", 22121) 157 | handleClick2(val, e) { 158 | $log("handleClick 方法正常执行。并收到参数:", val, e.target); 159 | message.success("handleClick 方法正常执行"); 160 | this.setState({ 161 | date: Date.now(), 162 | target: e.target 163 | }); 164 | } 165 | 166 | // @track("async.once", 22121) 167 | @track("async", 22121) 168 | handleClick3 = async (val, e) => { 169 | e.persist(); 170 | const response = await new Promise(resolve => { 171 | setTimeout(() => { 172 | resolve({ date: Date.now() }); 173 | }, 300); 174 | }); 175 | $log( 176 | "异步 handleClick 方法正常执行。并受到参数:", 177 | val, 178 | e.target, 179 | response.date 180 | ); 181 | message.success("异步 handleClick 方法正常执行"); 182 | this.setState({ 183 | date: response.date, 184 | target: e.target 185 | }); 186 | }; 187 | 188 | @track("async.once", 22121) 189 | handleClick4 = async (val, e) => { 190 | e.persist(); 191 | const response = await new Promise(resolve => { 192 | setTimeout(() => { 193 | resolve({ date: Date.now() }); 194 | }, 300); 195 | }); 196 | $log( 197 | "异步 handleClick 方法正常执行。并受到参数:", 198 | val, 199 | e.target, 200 | response.date 201 | ); 202 | message.success("异步 handleClick 方法正常执行"); 203 | this.setState({ 204 | date: response.date, 205 | target: e.target 206 | }); 207 | }; 208 | 209 | render() { 210 | return ( 211 |
212 |

213 | 如果我们需要在点击button时上报id为22121的埋点,示例如下: 214 |

215 | 221 | 222 |

223 | 如果需要在button点击事件发生后上报埋点,可增加after修饰符,示例如下: 224 |

225 | 231 | 232 | 236 |

237 | 如果button点击事件包含异步请求,可使用async修饰符,示例如下: 238 |

239 | 245 | 246 |

247 | 如果需要对来自 redux 中 action 248 | 事件进行埋点,我们可以在组件中包装一层async/await,示例如下: 249 |

250 | 251 |

252 | 如果埋点只需上报一次,可增加once修饰符,以异步埋点为例,示例如下: 253 |

254 | 260 | 261 |
262 | ); 263 | } 264 | } 265 | 266 | export default class WithTrigger extends Component { 267 | render() { 268 | return ( 269 | 270 | 271 | 272 | ); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-14 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Sunday 2019-07-14 00:40:31 am 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | module.exports = { 13 | collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"], 14 | setupFiles: ["react-app-polyfill/jsdom"], 15 | setupFilesAfterEnv: [], 16 | testMatch: [ 17 | "/src/**/__tests__/**/*.{js,jsx,ts,tsx}", 18 | "/src/**/*.{spec,test}.{js,jsx,ts,tsx}" 19 | ], 20 | testEnvironment: "jest-environment-jsdom-fourteen", 21 | transform: { 22 | "^.+\\.(js|jsx|ts|tsx)$": "/node_modules/babel-jest", 23 | "^.+\\.css$": "/config/jest/cssTransform.js", 24 | "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": 25 | "/config/jest/fileTransform.js" 26 | }, 27 | transformIgnorePatterns: [ 28 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$", 29 | "^.+\\.module\\.(css|sass|scss)$" 30 | ], 31 | modulePaths: [], 32 | moduleNameMapper: { 33 | "^react-native$": "react-native-web", 34 | "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy" 35 | }, 36 | moduleFileExtensions: [ 37 | "web.js", 38 | "js", 39 | "web.ts", 40 | "ts", 41 | "web.tsx", 42 | "tsx", 43 | "json", 44 | "web.jsx", 45 | "jsx", 46 | "node" 47 | ], 48 | watchPlugins: [ 49 | "jest-watch-typeahead/filename", 50 | "jest-watch-typeahead/testname" 51 | ] 52 | }; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "r-track", 3 | "version": "0.3.1", 4 | "description": "一个基于装饰器的埋点业务插件", 5 | "author": "LHammer ", 6 | "scripts": { 7 | "watch": "cross-env NODE_ENV=development rollup --config build/rollup.config.es.js --watch", 8 | "build": "yarn build:es && yarn build:browser", 9 | "build:browser": "NODE_ENV=production rollup --config build/rollup.config.browser.js && yarn size", 10 | "build:es": "NODE_ENV=production rollup --config build/rollup.config.es.js", 11 | "start": "node scripts/start.js", 12 | "build:docs": "node scripts/build.js", 13 | "test": "node scripts/test.js", 14 | "size": "gzip-size dist/r-track.min.js", 15 | "lint": "prettier --check src/**/*.js", 16 | "prepublishOnly": "yarn lint && yarn build" 17 | }, 18 | "main": "dist/r-track.min.js", 19 | "module": "dist/r-track.esm.js", 20 | "dependencies": {}, 21 | "devDependencies": { 22 | "@babel/core": "7.4.3", 23 | "@babel/plugin-proposal-decorators": "^7.4.4", 24 | "@svgr/webpack": "4.1.0", 25 | "@typescript-eslint/eslint-plugin": "1.6.0", 26 | "@typescript-eslint/parser": "1.6.0", 27 | "antd": "^3.22.0", 28 | "babel-eslint": "10.0.1", 29 | "babel-jest": "^24.8.0", 30 | "babel-loader": "8.0.5", 31 | "babel-plugin-import": "^1.12.0", 32 | "babel-plugin-named-asset-import": "^0.3.2", 33 | "babel-preset-react-app": "^9.0.0", 34 | "camelcase": "^5.2.0", 35 | "case-sensitive-paths-webpack-plugin": "2.2.0", 36 | "cross-env": "^5.2.0", 37 | "css-loader": "2.1.1", 38 | "dotenv": "6.2.0", 39 | "dotenv-expand": "4.2.0", 40 | "eslint": "^5.16.0", 41 | "eslint-config-react-app": "^4.0.1", 42 | "eslint-loader": "2.1.2", 43 | "eslint-plugin-flowtype": "2.50.1", 44 | "eslint-plugin-import": "2.16.0", 45 | "eslint-plugin-jsx-a11y": "6.2.1", 46 | "eslint-plugin-prettier": "^3.1.0", 47 | "eslint-plugin-react": "7.12.4", 48 | "eslint-plugin-react-hooks": "^1.5.0", 49 | "file-loader": "3.0.1", 50 | "fs-extra": "7.0.1", 51 | "gzip-size-cli": "^3.0.0", 52 | "highlight.js": "^9.15.9", 53 | "html-webpack-plugin": "4.0.0-beta.5", 54 | "husky": "^3.0.0", 55 | "identity-obj-proxy": "3.0.0", 56 | "is-wsl": "^1.1.0", 57 | "jest": "24.7.1", 58 | "jest-environment-jsdom-fourteen": "0.1.0", 59 | "jest-resolve": "24.7.1", 60 | "jest-watch-typeahead": "0.3.0", 61 | "lint-staged": "^9.2.0", 62 | "mini-css-extract-plugin": "0.5.0", 63 | "mobx": "^5.11.0", 64 | "mobx-react": "^6.1.1", 65 | "node-sass": "^4.12.0", 66 | "optimize-css-assets-webpack-plugin": "5.0.1", 67 | "pnp-webpack-plugin": "1.2.1", 68 | "postcss-flexbugs-fixes": "4.1.0", 69 | "postcss-loader": "3.0.0", 70 | "postcss-normalize": "7.0.1", 71 | "postcss-preset-env": "6.6.0", 72 | "postcss-safe-parser": "4.0.1", 73 | "prettier": "^1.18.2", 74 | "react": "^16.8.6", 75 | "react-app-polyfill": "^1.0.1", 76 | "react-dev-utils": "^9.0.1", 77 | "react-dom": "^16.8.6", 78 | "react-redux": "^7.1.0", 79 | "react-router": "^5.0.1", 80 | "react-router-dom": "^5.0.1", 81 | "redux": "^4.0.4", 82 | "redux-thunk": "^2.3.0", 83 | "resolve": "1.10.0", 84 | "rollup": "^1.16.6", 85 | "rollup-plugin-babel": "^4.3.3", 86 | "rollup-plugin-commonjs": "^10.0.1", 87 | "rollup-plugin-node-resolve": "^5.2.0", 88 | "rollup-plugin-replace": "^2.2.0", 89 | "rollup-plugin-uglify": "^6.0.2", 90 | "sass-loader": "7.1.0", 91 | "semver": "6.0.0", 92 | "style-loader": "0.23.1", 93 | "terser-webpack-plugin": "1.2.3", 94 | "ts-pnp": "1.1.2", 95 | "url-loader": "1.1.2", 96 | "webpack": "4.29.6", 97 | "webpack-dev-server": "3.2.1", 98 | "webpack-manifest-plugin": "2.0.4", 99 | "workbox-webpack-plugin": "4.2.0" 100 | }, 101 | "eslintConfig": { 102 | "extends": "react-app" 103 | }, 104 | "keywords": [ 105 | "react", 106 | "javascript", 107 | "track", 108 | "plugin", 109 | "decorator" 110 | ], 111 | "license": "MIT", 112 | "browserslist": { 113 | "production": [ 114 | ">0.2%", 115 | "not dead", 116 | "not op_mini all" 117 | ], 118 | "development": [ 119 | "last 1 chrome version", 120 | "last 1 firefox version", 121 | "last 1 safari version" 122 | ] 123 | }, 124 | "husky": { 125 | "hooks": { 126 | "pre-commit": "lint-staged" 127 | } 128 | }, 129 | "lint-staged": { 130 | "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ 131 | "prettier --write", 132 | "git add" 133 | ] 134 | }, 135 | "repository": { 136 | "type": "git", 137 | "url": "https://github.com/l-hammer/r-track.git" 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/l-hammer/r-track/40bb94e864d2837e53c09119c0a6dddb56fe40e6/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | r-track 24 | 25 | 26 | 27 |
28 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.BABEL_ENV = "production"; 3 | process.env.NODE_ENV = "production"; 4 | 5 | // Makes the script crash on unhandled rejections instead of silently 6 | // ignoring them. In the future, promise rejections that are not handled will 7 | // terminate the Node.js process with a non-zero exit code. 8 | process.on("unhandledRejection", err => { 9 | throw err; 10 | }); 11 | 12 | // Ensure environment variables are read. 13 | require("../config/env"); 14 | 15 | const path = require("path"); 16 | const chalk = require("react-dev-utils/chalk"); 17 | const fs = require("fs-extra"); 18 | const webpack = require("webpack"); 19 | const configFactory = require("../config/webpack.config"); 20 | const paths = require("../config/paths"); 21 | const checkRequiredFiles = require("react-dev-utils/checkRequiredFiles"); 22 | const formatWebpackMessages = require("react-dev-utils/formatWebpackMessages"); 23 | const printHostingInstructions = require("react-dev-utils/printHostingInstructions"); 24 | const FileSizeReporter = require("react-dev-utils/FileSizeReporter"); 25 | const printBuildError = require("react-dev-utils/printBuildError"); 26 | 27 | const measureFileSizesBeforeBuild = 28 | FileSizeReporter.measureFileSizesBeforeBuild; 29 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 30 | const useYarn = fs.existsSync(paths.yarnLockFile); 31 | 32 | // These sizes are pretty large. We'll warn for bundles exceeding them. 33 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 34 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 35 | 36 | const isInteractive = process.stdout.isTTY; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1); 41 | } 42 | 43 | // Generate configuration 44 | const config = configFactory("production"); 45 | 46 | // We require that you explicitly set browsers and do not fall back to 47 | // browserslist defaults. 48 | const { checkBrowsers } = require("react-dev-utils/browsersHelper"); 49 | checkBrowsers(paths.appPath, isInteractive) 50 | .then(() => { 51 | // First, read the current file sizes in build directory. 52 | // This lets us display how much they changed later. 53 | return measureFileSizesBeforeBuild(paths.appBuild); 54 | }) 55 | .then(previousFileSizes => { 56 | // Remove all content but keep the directory so that 57 | // if you're in it, you don't end up in Trash 58 | fs.emptyDirSync(paths.appBuild); 59 | // Merge with the public folder 60 | copyPublicFolder(); 61 | // Start the webpack build 62 | return build(previousFileSizes); 63 | }) 64 | .then( 65 | ({ stats, previousFileSizes, warnings }) => { 66 | if (warnings.length) { 67 | console.log(chalk.yellow("Compiled with warnings.\n")); 68 | console.log(warnings.join("\n\n")); 69 | console.log( 70 | "\nSearch for the " + 71 | chalk.underline(chalk.yellow("keywords")) + 72 | " to learn more about each warning." 73 | ); 74 | console.log( 75 | "To ignore, add " + 76 | chalk.cyan("// eslint-disable-next-line") + 77 | " to the line before.\n" 78 | ); 79 | } else { 80 | console.log(chalk.green("Compiled successfully.\n")); 81 | } 82 | 83 | console.log("File sizes after gzip:\n"); 84 | printFileSizesAfterBuild( 85 | stats, 86 | previousFileSizes, 87 | paths.appBuild, 88 | WARN_AFTER_BUNDLE_GZIP_SIZE, 89 | WARN_AFTER_CHUNK_GZIP_SIZE 90 | ); 91 | console.log(); 92 | 93 | const appPackage = require(paths.appPackageJson); 94 | const publicUrl = paths.publicUrl; 95 | const publicPath = config.output.publicPath; 96 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 97 | printHostingInstructions( 98 | appPackage, 99 | publicUrl, 100 | publicPath, 101 | buildFolder, 102 | useYarn 103 | ); 104 | }, 105 | err => { 106 | console.log(chalk.red("Failed to compile.\n")); 107 | printBuildError(err); 108 | process.exit(1); 109 | } 110 | ) 111 | .catch(err => { 112 | if (err && err.message) { 113 | console.log(err.message); 114 | } 115 | process.exit(1); 116 | }); 117 | 118 | // Create the production build and print the deployment instructions. 119 | function build(previousFileSizes) { 120 | // We used to support resolving modules according to `NODE_PATH`. 121 | // This now has been deprecated in favor of jsconfig/tsconfig.json 122 | // This lets you use absolute paths in imports inside large monorepos: 123 | if (process.env.NODE_PATH) { 124 | console.log( 125 | chalk.yellow( 126 | "Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app." 127 | ) 128 | ); 129 | console.log(); 130 | } 131 | 132 | console.log("Creating an optimized production build..."); 133 | 134 | const compiler = webpack(config); 135 | return new Promise((resolve, reject) => { 136 | compiler.run((err, stats) => { 137 | let messages; 138 | if (err) { 139 | if (!err.message) { 140 | return reject(err); 141 | } 142 | messages = formatWebpackMessages({ 143 | errors: [err.message], 144 | warnings: [] 145 | }); 146 | } else { 147 | messages = formatWebpackMessages( 148 | stats.toJson({ all: false, warnings: true, errors: true }) 149 | ); 150 | } 151 | if (messages.errors.length) { 152 | // Only keep the first error. Others are often indicative 153 | // of the same problem, but confuse the reader with noise. 154 | if (messages.errors.length > 1) { 155 | messages.errors.length = 1; 156 | } 157 | return reject(new Error(messages.errors.join("\n\n"))); 158 | } 159 | if ( 160 | process.env.CI && 161 | (typeof process.env.CI !== "string" || 162 | process.env.CI.toLowerCase() !== "false") && 163 | messages.warnings.length 164 | ) { 165 | console.log( 166 | chalk.yellow( 167 | "\nTreating warnings as errors because process.env.CI = true.\n" + 168 | "Most CI servers set it automatically.\n" 169 | ) 170 | ); 171 | return reject(new Error(messages.warnings.join("\n\n"))); 172 | } 173 | 174 | return resolve({ 175 | stats, 176 | previousFileSizes, 177 | warnings: messages.warnings 178 | }); 179 | }); 180 | }); 181 | } 182 | 183 | function copyPublicFolder() { 184 | fs.copySync(paths.appPublic, paths.appBuild, { 185 | dereference: true, 186 | filter: file => file !== paths.appHtml 187 | }); 188 | } 189 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | 18 | const fs = require('fs'); 19 | const chalk = require('react-dev-utils/chalk'); 20 | const webpack = require('webpack'); 21 | const WebpackDevServer = require('webpack-dev-server'); 22 | const clearConsole = require('react-dev-utils/clearConsole'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const { 25 | choosePort, 26 | createCompiler, 27 | prepareProxy, 28 | prepareUrls, 29 | } = require('react-dev-utils/WebpackDevServerUtils'); 30 | const openBrowser = require('react-dev-utils/openBrowser'); 31 | const paths = require('../config/paths'); 32 | const configFactory = require('../config/webpack.config'); 33 | const createDevServerConfig = require('../config/webpackDevServer.config'); 34 | 35 | const useYarn = fs.existsSync(paths.yarnLockFile); 36 | const isInteractive = process.stdout.isTTY; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1); 41 | } 42 | 43 | // Tools like Cloud9 rely on this. 44 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 45 | const HOST = process.env.HOST || '0.0.0.0'; 46 | 47 | if (process.env.HOST) { 48 | console.log( 49 | chalk.cyan( 50 | `Attempting to bind to HOST environment variable: ${chalk.yellow( 51 | chalk.bold(process.env.HOST) 52 | )}` 53 | ) 54 | ); 55 | console.log( 56 | `If this was unintentional, check that you haven't mistakenly set it in your shell.` 57 | ); 58 | console.log( 59 | `Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}` 60 | ); 61 | console.log(); 62 | } 63 | 64 | // We require that you explicitly set browsers and do not fall back to 65 | // browserslist defaults. 66 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 67 | checkBrowsers(paths.appPath, isInteractive) 68 | .then(() => { 69 | // We attempt to use the default port but if it is busy, we offer the user to 70 | // run on a different port. `choosePort()` Promise resolves to the next free port. 71 | return choosePort(HOST, DEFAULT_PORT); 72 | }) 73 | .then(port => { 74 | if (port == null) { 75 | // We have not found a port. 76 | return; 77 | } 78 | const config = configFactory('development'); 79 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 80 | const appName = require(paths.appPackageJson).name; 81 | const useTypeScript = fs.existsSync(paths.appTsConfig); 82 | const urls = prepareUrls(protocol, HOST, port); 83 | const devSocket = { 84 | warnings: warnings => 85 | devServer.sockWrite(devServer.sockets, 'warnings', warnings), 86 | errors: errors => 87 | devServer.sockWrite(devServer.sockets, 'errors', errors), 88 | }; 89 | // Create a webpack compiler that is configured with custom messages. 90 | const compiler = createCompiler({ 91 | appName, 92 | config, 93 | devSocket, 94 | urls, 95 | useYarn, 96 | useTypeScript, 97 | webpack, 98 | }); 99 | // Load proxy config 100 | const proxySetting = require(paths.appPackageJson).proxy; 101 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 102 | // Serve webpack assets generated by the compiler over a web server. 103 | const serverConfig = createDevServerConfig( 104 | proxyConfig, 105 | urls.lanUrlForConfig 106 | ); 107 | const devServer = new WebpackDevServer(compiler, serverConfig); 108 | // Launch WebpackDevServer. 109 | devServer.listen(port, HOST, err => { 110 | if (err) { 111 | return console.log(err); 112 | } 113 | if (isInteractive) { 114 | clearConsole(); 115 | } 116 | 117 | // We used to support resolving modules according to `NODE_PATH`. 118 | // This now has been deprecated in favor of jsconfig/tsconfig.json 119 | // This lets you use absolute paths in imports inside large monorepos: 120 | if (process.env.NODE_PATH) { 121 | console.log( 122 | chalk.yellow( 123 | 'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.' 124 | ) 125 | ); 126 | console.log(); 127 | } 128 | 129 | console.log(chalk.cyan('Starting the development server...\n')); 130 | openBrowser(urls.localUrlForBrowser); 131 | }); 132 | 133 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 134 | process.on(sig, function() { 135 | devServer.close(); 136 | process.exit(); 137 | }); 138 | }); 139 | }) 140 | .catch(err => { 141 | if (err && err.message) { 142 | console.log(err.message); 143 | } 144 | process.exit(1); 145 | }); 146 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--watchAll') === -1 45 | ) { 46 | // https://github.com/facebook/create-react-app/issues/5210 47 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 48 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 49 | } 50 | 51 | 52 | jest.run(argv); 53 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-08 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Thursday 2019-08-22 16:23:39 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import ReactDOM from "react-dom"; 13 | import { isElement, isVisible } from "./utils/dom"; 14 | import { isObject, zipObject, clearTimeoutQueue } from "./utils/helper"; 15 | import { vaildEvent, vaildRC } from "./utils/error"; 16 | import VisMonitor from "./utils/vis-monitor"; 17 | 18 | const ONCE = "once"; 19 | const modifiers = { 20 | UVPV: "UVPV", // 特殊的修饰符 21 | TONP: "TONP", // 特殊的修饰符,表示页面停留时长 22 | TRIGGER: "trigger", 23 | TRIGGER_AFTER: "trigger.after", 24 | ASYNC: "async", 25 | ASYNC_DELAY: "async.delay", 26 | SHOW: "show" 27 | }; 28 | 29 | /** 30 | * @desc inject 装饰器 31 | * @param {Function} reaction 如果为 Mobx Class 则需要注入 mobx.reaction 来监听通过异步action 对 state 的改变 32 | * @param {Object} trackEvents 当前页面需要的埋点事件 33 | */ 34 | export const inject = (...props) => target => { 35 | Object.assign(target.prototype, ...props); 36 | }; 37 | 38 | /** 39 | * @desc track 埋点装饰器 40 | * @param {String} modifier 修饰符,对应的埋点类型 41 | * @param {Number | String} eventId 埋点事件id 42 | * @param {Object} params 自定义参数,目前支持tateKey、propKey、delay、ref 43 | * 44 | * @property[stateKey] 对应组件 state 或者 mobx observable 中 key 45 | * @property[propKey] 对应组件 props 中 key 46 | * @property[delay] 埋点延迟时间 47 | * @property[ref] 对应 DOM 的引用,避免小于埋点延迟时间内发生DOM影藏造成埋点继续上报的问题 48 | */ 49 | export function track(modifier, eventId, params = {}) { 50 | const [mdfs] = zipObject(modifiers); 51 | 52 | // 获取模块对应的埋点的适配器 53 | function getTrackEvents(ctx) { 54 | return ctx.trackEvents || ctx.props.trackEvents; 55 | } 56 | 57 | // 当前实例是否为react class组件 58 | function checkRC(ctx) { 59 | return ctx.isReactComponent; 60 | } 61 | 62 | // 生成唯一的once标记属性 63 | function createOnceProp(name, eventId) { 64 | return `${name}_${eventId}_${ONCE}`; 65 | } 66 | 67 | // 描述符value适配器 68 | function valueAdapter(ctx, value, initializer, args) { 69 | if (value) { 70 | return value.bind(ctx); 71 | } 72 | if (initializer) { 73 | return initializer.apply(ctx, args); 74 | } 75 | } 76 | 77 | // 生成不同队列,即value执行前的值 78 | function queueAdapter(ctx, fn, tck, ops) { 79 | if (ops.after) { 80 | return [fn, ops.isRC ? () => ctx.setState({}, tck) : tck]; 81 | } else { 82 | return [tck, fn]; 83 | } 84 | } 85 | 86 | if (!mdfs.includes(modifier.replace(/\.once/g, ""))) { 87 | throw new Error(`modifier '${modifier}' does not exist`); 88 | } 89 | return (_, name, descriptor) => { 90 | let handler; 91 | const { value, initializer } = descriptor; 92 | // 页面初始化埋点 93 | if (modifier.includes(modifiers.UVPV)) { 94 | handler = function(...args) { 95 | vaildRC(this.isReactComponent); 96 | 97 | const evts = getTrackEvents(this); 98 | const fn = valueAdapter(this, value, initializer, args); 99 | const tck = () => { 100 | const context = { ...this.state, ...this.props }; 101 | 102 | evts[modifier].call(null, context, ...args); 103 | }; 104 | 105 | return [tck, fn].forEach(sub => sub(...args)); 106 | }; 107 | } 108 | // 设置进入页面时间 109 | else if (modifier.includes(modifiers.TONP)) { 110 | handler = function(...args) { 111 | const isRC = vaildRC(this.isReactComponent); // 是否为 react 组件 112 | const evts = getTrackEvents(this); 113 | const tck = () => { 114 | const stt = Date.now() - this.__trackPageEntryTime; 115 | const context = { ...this.state, ...this.props }; 116 | 117 | evts[modifier].call(null, context, { stt }); 118 | }; 119 | 120 | this.__trackPageEntryTime = Date.now(); 121 | // 页面卸载前上报埋点 122 | if (isRC && !this.componentWillUnmount) { 123 | this.$needMergeTONPWillUnmount = false; 124 | this.componentWillUnmount = () => tck(); 125 | } else if ( 126 | this.componentWillUnmount && 127 | this.$needMergeTONPWillUnmount !== false 128 | ) { 129 | const willUnmountRef = this.componentWillUnmount; 130 | 131 | this.componentWillUnmount = () => { 132 | willUnmountRef.apply(this); 133 | tck(); 134 | }; 135 | } 136 | 137 | return value 138 | ? value.apply(this, args) 139 | : initializer.apply(this, args)(...args); 140 | }; 141 | } 142 | // 区域曝光埋点 143 | else if (modifier.includes(modifiers.SHOW)) { 144 | handler = function(...args) { 145 | const isRC = vaildRC(this.isReactComponent); // 是否为 react 组件 146 | const evts = getTrackEvents(this); 147 | 148 | function tck(event, params) { 149 | const context = { ...this.state, ...this.props }; 150 | 151 | evts[event].call(null, context, params); 152 | } 153 | // 绑定滚动监听器 154 | function visMonitor() { 155 | const props = Object.keys(this).filter(k => isObject(this[k])); 156 | 157 | props.forEach(prop => { 158 | let ele = this[prop].current || this[prop]; 159 | 160 | if ( 161 | /TrackRef$/.test(prop) && 162 | (isElement(ele) || ele.isReactComponent) && 163 | !ele.$visMonitor 164 | ) { 165 | ele = ele.isReactComponent && ReactDOM.findDOMNode(ele); 166 | 167 | const vm = new VisMonitor(ele); 168 | const { trackOnce, trackEvent, trackParams } = ele.dataset; 169 | 170 | (trackOnce ? vm.$once : vm.$on).call( 171 | vm, 172 | "fullyvisible", 173 | tck.bind(this, trackEvent, trackParams) 174 | ); 175 | ele.$visMonitor = vm; 176 | } 177 | }); 178 | } 179 | 180 | // 只执行一次 181 | if (isRC && !this.$isMounted) { 182 | const didMountRef = this.componentDidMount; 183 | 184 | this.componentDidMount = () => { 185 | didMountRef && didMountRef.apply(this); 186 | this.$isMounted = true; // 编辑是否已经挂载 187 | visMonitor.apply(this); 188 | }; 189 | } 190 | // DOM update 更新滚动监听器 191 | if (isRC && !this.componentDidUpdate) { 192 | this.$needMergeSHOWDidUpdate = false; 193 | this.componentDidUpdate = () => visMonitor.apply(this); 194 | } else if ( 195 | this.componentDidUpdate && 196 | this.$needMergeSHOWDidUpdate !== false 197 | ) { 198 | this.$didUpdateHookRef = 199 | this.$didUpdateHookRef || this.componentDidUpdate; 200 | this.componentDidUpdate = () => { 201 | this.$didUpdateHookRef.apply(this); 202 | visMonitor.apply(this); 203 | }; 204 | } 205 | return value.apply(this, args); 206 | }; 207 | } 208 | // 事件行为埋点(支持同步、异步) 209 | else if ( 210 | (modifier.includes(modifiers.TRIGGER) || 211 | modifier.includes(modifiers.ASYNC)) && 212 | (!params.stateKey && !params.propKey) 213 | ) { 214 | handler = function(...args) { 215 | let tck; 216 | let context = this; 217 | const isRC = checkRC(this); // 是否为 react 组件 218 | const evts = getTrackEvents(this); 219 | const onceProp = createOnceProp(name, eventId); 220 | const fn = valueAdapter(this, value, initializer, args); 221 | const after = 222 | modifier.includes(modifiers.TRIGGER_AFTER) || 223 | modifier.includes(modifiers.ASYNC); 224 | 225 | tck = () => { 226 | if (this[onceProp]) return; // 如果存在once修饰符,且为true则直接返回 227 | vaildEvent(evts, eventId); // 检测eventId是否合法 228 | isRC && (context = { ...this.state, ...this.props }); 229 | evts[eventId].call(null, context, ...args); 230 | modifier.includes(ONCE) && (this[onceProp] = true); 231 | }; 232 | 233 | if ( 234 | modifier === modifiers.ASYNC_DELAY || 235 | modifier === modifiers.ASYNC_DELAY + ".once" 236 | ) { 237 | !this.tckTimerQueue && (this.tckTimerQueue = {}); // 定时器队列 238 | tck = () => { 239 | const { delay = 0, ref } = params; 240 | const ele = this[ref] || document; 241 | const timer = this.tckTimerQueue[eventId]; 242 | 243 | timer && clearTimeout(timer); 244 | this.tckTimerQueue[eventId] = setTimeout(() => { 245 | isRC && (context = { ...this.state, ...this.props }); 246 | isVisible(ele) && evts[eventId].call(null, context, ...args); 247 | clearTimeout(this.tckTimerQueue[eventId]); 248 | }, delay); 249 | }; 250 | 251 | // 页面卸载时清除进行中的定时器 252 | if (isRC && !this.componentWillUnmount) { 253 | this.$needMergeWillUnmount = false; 254 | this.componentWillUnmount = () => { 255 | clearTimeoutQueue(this.tckTimerQueue); 256 | }; 257 | } else if ( 258 | this.componentWillUnmount && 259 | this.$needMergeWillUnmount !== false 260 | ) { 261 | const willUnmountRef = this.componentWillUnmount; 262 | this.componentWillUnmount = () => { 263 | willUnmountRef.apply(this); 264 | clearTimeoutQueue(this.tckTimerQueue); 265 | }; 266 | } 267 | } 268 | 269 | const queue = queueAdapter(this, fn, tck, { isRC, after }); 270 | 271 | if (modifier.includes(modifiers.ASYNC)) { 272 | // 异步 273 | return (async () => { 274 | for (const iter of queue) { 275 | await iter(...args); 276 | } 277 | })(); 278 | } else { 279 | // 同步 280 | return queue.forEach(sub => sub(...args)); 281 | } 282 | }; 283 | } 284 | 285 | if (value) { 286 | descriptor.value = function(...args) { 287 | return handler.apply(this, args); 288 | }; 289 | } 290 | // 兼容箭头函数 https://github.com/MuYunyun/diana/issues/7 291 | if (initializer) { 292 | descriptor.initializer = function() { 293 | return handler.bind(this); 294 | }; 295 | } 296 | }; 297 | } 298 | -------------------------------------------------------------------------------- /src/utils/dom.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-09 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Tuesday 2019-07-09 18:09:50 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | /** 13 | * @desc 是否为元素几点 14 | * 15 | * @param {DOMElement} ele 一个 DOM 元素 16 | * @return {Boolean} 17 | */ 18 | export const isElement = ele => ele && ele.nodeType === 1; 19 | 20 | /** 21 | * @desc 获取 DOM CSS 属性的值 22 | * 23 | * @param {DOMElement} ele A DOM 元素 24 | * @returns {String} 25 | */ 26 | export function getStylePropValue(ele, prop) { 27 | return window.getComputedStyle(ele).getPropertyValue(prop); 28 | } 29 | 30 | /** 31 | * @desc 元素是否在可视区域可见 32 | * 33 | * @param {Object} rect 元素大小及相对可视区域的位置信息 34 | * @returns {Boolean} true => 可见 false => 不可见 35 | */ 36 | export function isInViewport(rect, viewport) { 37 | if (!rect || (rect.width <= 0 || rect.height <= 0)) { 38 | return false; 39 | } 40 | return ( 41 | rect.bottom > 0 && 42 | rect.right > 0 && 43 | rect.top < viewport.height && 44 | rect.left < viewport.width 45 | ); 46 | } 47 | 48 | /** 49 | * @desc 元素是否隐藏 50 | * 51 | * @param {DOMElement} ele A DOM 元素 52 | * @returns {Boolean} true => 未隐藏可见 false => 隐藏不可见 53 | */ 54 | export function isVisible(ele) { 55 | if (ele === window.document) { 56 | return true; 57 | } 58 | if (!ele || !ele.parentNode) { 59 | return false; 60 | } 61 | 62 | const parent = ele.parentNode; 63 | const visibility = getStylePropValue(ele, "visibility"); 64 | const display = getStylePropValue(ele, "display"); 65 | 66 | if (visibility === "hidden" || display === "none") { 67 | return false; 68 | } 69 | return parent ? isVisible(parent) : true; 70 | } 71 | -------------------------------------------------------------------------------- /src/utils/error.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-22 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Tuesday 2019-07-23 11:54:20 am 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import { isFun } from "./helper"; 13 | 14 | /** 15 | * @desc chenck 埋点ID是否合法 16 | * 17 | * @param {Object} events 当前模块的所有埋点 18 | * @param {Object} eventId 埋点事件id 19 | */ 20 | export const vaildEvent = (events, eventId) => { 21 | if (!isFun(events[eventId])) { 22 | throw new Error(`track eventId '${eventId}' does not exist`); 23 | } 24 | }; 25 | 26 | /** 27 | * @desc check 异步埋点监听key是否存在 28 | */ 29 | export const vaildWatchKey = (stateKey, propKey) => { 30 | if (!stateKey && !propKey) { 31 | throw new Error( 32 | `Missing arguments.{stateKey} or arguments.{propKey} in async track` 33 | ); 34 | } 35 | }; 36 | 37 | /** 38 | * @desc check 当前实例是否为react组件 39 | */ 40 | export const vaildRC = isRC => { 41 | if (!isRC) { 42 | throw new Error(`The current instance is not a react component`); 43 | } else { 44 | return isRC; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/utils/helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-07-11 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Monday 2019-08-12 21:30:01 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | /** 13 | * @desc 获取对象的键值 14 | * 15 | * @param {Object} value 16 | * @returns {Array} [keys, values] 17 | */ 18 | export function zipObject(value) { 19 | return [Object.values(value), Object.keys(value)]; 20 | } 21 | 22 | /** 23 | * @desc 判断给定变量是否为一个函数 24 | * 25 | * @param {*} v 26 | * @return {Boolean} 27 | */ 28 | export const isFun = v => typeof v === "function" || false; 29 | 30 | /** 31 | * @desc 判断给定变量是否为一个对象 32 | * 33 | * @param {*} v 34 | * @return {Boolean} 35 | */ 36 | export const isObject = val => { 37 | return val !== null && typeof val === "object"; 38 | }; 39 | 40 | /** 41 | * @desc 清除定时器 42 | */ 43 | export const clearTimeoutQueue = (queue = []) => { 44 | Object.keys(queue).forEach(timer => { 45 | clearTimeout(queue[timer]); 46 | }); 47 | }; 48 | 49 | /** 50 | * @desc 防抖函数,至少间隔200毫秒执行一次 51 | * 52 | * @param {Function} fn callback 53 | * @param {Number} [ms=200] 默认200毫秒 54 | * @returns {Function} 55 | */ 56 | export function debounce(fn, ms = 200) { 57 | let timeoutId; 58 | return function(...args) { 59 | clearTimeout(timeoutId); 60 | timeoutId = setTimeout(() => fn.apply(this, args), ms); 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/utils/vis-monitor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Created Date: 2019-08-11 3 | * Author: 宋慧武 4 | * ------ 5 | * Last Modified: Monday 2019-08-12 17:32:20 pm 6 | * Modified By: the developer formerly known as 宋慧武 at 7 | * ------ 8 | * HISTORY: 9 | * ------ 10 | * Javascript will save your soul! 11 | */ 12 | import { isElement, isVisible, isInViewport } from "./dom"; 13 | import { isFun, debounce } from "./helper"; 14 | 15 | /** 16 | * @class 17 | * @name VisMonitor 18 | * 19 | * @desc 目标元素控制器 20 | */ 21 | export default class VisMonitor { 22 | constructor(ele, ref, refwin = window) { 23 | if (!isElement(ele)) { 24 | throw new Error("not an element node"); 25 | } 26 | this.ele = ele; 27 | this.ref = ref; 28 | this.refWin = refwin; 29 | this.started = false; 30 | this.prevPerc = null; // 保存前一次曝光百分比 31 | this.listeners = {}; 32 | this.removeScrollLisener = null; 33 | this.init(); 34 | } 35 | 36 | init() { 37 | if (!this.started) { 38 | const listener = debounce(this.visibilitychange.bind(this)); 39 | 40 | listener(); 41 | this.removeScrollLisener = (listener => { 42 | if (this.ref) { 43 | return this.ref.$on("scroll", listener); 44 | } else { 45 | this.refWin.addEventListener("scroll", listener, false); 46 | return () => 47 | this.refWin.removeEventListener("scroll", listener, false); 48 | } 49 | })(listener); 50 | this.started = true; 51 | } 52 | } 53 | 54 | viewport() { 55 | const win = this.refWin; 56 | 57 | return { 58 | height: win.innerHeight, 59 | width: win.innerWidth 60 | }; 61 | } 62 | 63 | /** 64 | * 监听自定义事件 65 | */ 66 | $on(evt, cbk) { 67 | const queue = this.listeners[evt] || (this.listeners[evt] = []); 68 | 69 | queue.push(cbk); 70 | return this; 71 | } 72 | 73 | /** 74 | * 移除监听自定义事件 75 | */ 76 | $off(evt, cbk) { 77 | if (!cbk) return; 78 | 79 | let queue = this.listeners[evt]; 80 | let v; 81 | let i = queue.length; 82 | 83 | while (i--) { 84 | v = queue[i]; 85 | if (v === cbk || v.cbk === cbk) { 86 | queue.splice(i, 1); 87 | break; 88 | } 89 | } 90 | return this; 91 | } 92 | 93 | /** 94 | * 监听自定义事件,但只触发一次 95 | */ 96 | $once(evt, cbk) { 97 | const on = (...args) => { 98 | this.$off(evt, on); 99 | cbk.apply(this, args); 100 | }; 101 | 102 | on.cbk = cbk; 103 | this.$on(evt, on); 104 | return this; 105 | } 106 | 107 | /** 108 | * 触发当前实例的监听回调 109 | */ 110 | $emit(evt, ...args) { 111 | const queue = this.listeners[evt] || []; 112 | 113 | queue.forEach(sub => sub.apply(this, args)); 114 | return this; 115 | } 116 | 117 | /** 118 | * 计算元素可见比例,如果比例为100%,则触发 fullyvisible 事件 119 | */ 120 | visibilitychange() { 121 | const rect = this.ele.getBoundingClientRect(); 122 | const view = this.viewport(); 123 | 124 | if (!isInViewport(rect, view) || !isVisible(this.ele)) { 125 | this.prevPerc = 0; 126 | return 0; 127 | } 128 | 129 | let vh = 0; 130 | let vw = 0; 131 | let perc = 0; 132 | 133 | if (rect.top >= 0) { 134 | vh = Math.min(rect.height, view.height - rect.top); 135 | } else if (rect.bottom > 0) { 136 | vh = Math.min(view.height, rect.bottom); 137 | } 138 | if (rect.left >= 0) { 139 | vw = Math.min(rect.width, view.width - rect.left); 140 | } else if (rect.right > 0) { 141 | vw = Math.min(view.width, rect.right); 142 | } 143 | perc = (vh * vw) / (rect.height * rect.width); 144 | 145 | if (this.prevPerc !== 1 && perc === 1) { 146 | this.$emit("fullyvisible"); 147 | this.prevPerc = perc; 148 | } 149 | } 150 | 151 | /** 152 | * 销毁当前实例的事件 153 | */ 154 | destroy() { 155 | isFun(this.removeScrollLisener) && this.removeScrollLisener(); 156 | } 157 | } 158 | --------------------------------------------------------------------------------