├── CHANGELOG.md ├── example └── src │ └── page │ ├── index │ ├── container │ │ ├── index.less │ │ └── index.js │ ├── main.html │ └── main.js │ └── pindex │ ├── container │ ├── index.less │ └── index.js │ ├── main.js │ └── main.html ├── .stylelintrc.js ├── postcss.config.js ├── src ├── index.less ├── pindex.js ├── index.js └── spin.js ├── .npmignore ├── config ├── steamer.config.js └── project.js ├── .gitignore ├── test └── unit │ ├── helpers │ └── enzyme.helper.js │ ├── specs │ └── spin.spec.js │ ├── index.js │ └── karma.conf.js ├── .eslintrc.js ├── tools ├── feature │ ├── tsconfig.json │ └── feature.js ├── rules │ ├── fileRules.js │ ├── htmlRules.js │ ├── jsRules.js │ └── styleRules.js ├── plugins │ ├── basePlugins.js │ └── resourcePlugins.js ├── optimization │ └── index.js ├── script.js ├── server.js └── webpack.base.js ├── .editorconfig ├── .babelrc ├── jest.conf.json ├── .steamer └── steamer-react-component.js ├── LICENSE ├── README.md └── package.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.0.1 2 | - update dependency 3 | -------------------------------------------------------------------------------- /example/src/page/index/container/index.less: -------------------------------------------------------------------------------- 1 | .hello { 2 | font-size: 28px; 3 | } -------------------------------------------------------------------------------- /example/src/page/pindex/container/index.less: -------------------------------------------------------------------------------- 1 | .hello { 2 | font-size: 28px; 3 | } -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "@alloyteam/stylelint-config-standard", 3 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import')(), 4 | require('autoprefixer')() 5 | ] 6 | } -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | #spin { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | margin: auto; 8 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | node_modules.7z 3 | .DS_Store 4 | .webpack_cache 5 | .cache 6 | .happypack 7 | npm-debug.log 8 | yarn-error.log 9 | coverage -------------------------------------------------------------------------------- /config/steamer.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "webserver": "//localhost:9000/", 3 | "cdn": "//localhost:8000/", 4 | "port": "9000", 5 | "route": "/", 6 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | node_modules.7z 3 | .svn 4 | .DS_Store 5 | .cache 6 | .happypack 7 | npm-debug.log 8 | yarn-error.log 9 | jest 10 | dist 11 | test/**/coverage 12 | dev 13 | -------------------------------------------------------------------------------- /test/unit/helpers/enzyme.helper.js: -------------------------------------------------------------------------------- 1 | // ES6 Import 2 | import Enzyme from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-15'; 4 | 5 | Enzyme.configure({ adapter: new Adapter() }); 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": {}, 3 | "extends": [ 4 | "eslint-config-alloy/react", 5 | ], 6 | "plugins": [], 7 | "rules": { 8 | 'one-var': 'off' 9 | }, 10 | "globals": {} 11 | }; -------------------------------------------------------------------------------- /tools/feature/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "commonjs", 7 | "target": "es5", 8 | "jsx": "react" 9 | }, 10 | "include": ["./src/**/*"] 11 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [{package.json,.travis.yml}] 15 | indent_style = space 16 | indent_size = 2 -------------------------------------------------------------------------------- /example/src/page/pindex/main.js: -------------------------------------------------------------------------------- 1 | /** @jsx h */ 2 | import Preact, { h, Component } from 'preact'; 3 | 4 | import Main from './container/index'; 5 | 6 | const rootEl = document.getElementById('root'); 7 | 8 | const render = (Component) => { 9 | ReactDOM.render( 10 | , 11 | rootEl 12 | ); 13 | }; 14 | 15 | render(Main); -------------------------------------------------------------------------------- /example/src/page/index/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | react component 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/src/page/pindex/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | react component 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-decorators-legacy" 4 | ], 5 | "presets": [ 6 | ["es2015", {"loose": true, "modules": false}], 7 | "react" 8 | ], 9 | "env": { 10 | "test": { 11 | "presets": [ 12 | ["es2015", {"loose": true, "modules": false}], 13 | "react", 14 | "airbnb" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/src/page/index/container/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Spinner from 'index'; 3 | 4 | import './index.less'; 5 | 6 | class Main extends Component { 7 | 8 | constructor(props, context) { 9 | super(props, context); 10 | } 11 | 12 | render() { 13 | 14 | return ( 15 | 16 | ); 17 | } 18 | } 19 | 20 | export default Main; -------------------------------------------------------------------------------- /test/unit/specs/spin.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Spin from '../../../src/index.js'; 4 | import React from 'react'; 5 | // import { expect } from 'chai'; 6 | import { mount, shallow } from 'enzyme'; 7 | 8 | describe('test', () => { 9 | 10 | it('spin', function() { 11 | const wrapper = mount(); 12 | expect(wrapper.find('div')).to.have.length(1); 13 | }); 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /example/src/page/pindex/container/index.js: -------------------------------------------------------------------------------- 1 | /** @jsx h */ 2 | import Preact, { h, Component } from 'preact'; 3 | import Spinner from 'index'; 4 | 5 | import './index.less'; 6 | 7 | class Main extends Component { 8 | 9 | constructor(props, context) { 10 | super(props, context); 11 | } 12 | 13 | render() { 14 | 15 | return ( 16 | 17 | ); 18 | } 19 | } 20 | 21 | export default Main; -------------------------------------------------------------------------------- /example/src/page/index/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | 5 | import Main from './container/index'; 6 | 7 | const rootEl = document.getElementById('root'); 8 | 9 | const render = (Component) => { 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | rootEl 15 | ); 16 | }; 17 | 18 | render(Main); 19 | 20 | // Hot Module Replacement API 21 | if (module.hot) { 22 | module.hot.accept('./container/index', () => { 23 | const NextApp = require('./container/index').default; 24 | render(NextApp); 25 | }); 26 | } -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | // Polyfill fn.bind() for PhantomJS 2 | /* eslint-disable no-extend-native */ 3 | Function.prototype.bind = require('function-bind'); 4 | 5 | // 引入所有测试文件 (以 .spec.js 结尾) 6 | const testsContext = require.context('./specs', true, /\.spec$/); 7 | testsContext.keys().forEach(testsContext); 8 | 9 | // 引入除了main.js的所有源文件以做覆盖率测试 10 | // 你也可以修改配置只测试一部分js文件 11 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/); 12 | srcContext.keys().forEach(srcContext); 13 | 14 | // 引入enyzme 需要用的helper,参考 15 | // http://airbnb.io/enzyme/docs/installation/index.html 16 | // https://github.com/airbnb/enzyme/issues/1257 17 | var testHelpers = require.context('./helpers'); 18 | testHelpers.keys().forEach(testHelpers); 19 | -------------------------------------------------------------------------------- /jest.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "jsx" 5 | ], 6 | "moduleDirectories": [ 7 | "node_modules" 8 | ], 9 | "coverageDirectory": "/test/jest/coverage", 10 | "testMatch": [ 11 | "**/test/jest/**/?(*.)(spec|test).js?(x)" 12 | ], 13 | "setupFiles": [ 14 | "/test/jest/helpers/enzyme.helper.js" 15 | ], 16 | "moduleNameMapper": { 17 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/test/jest/mocks/fileMock.js", 18 | "\\.(css|less|scss|sass|styl)$": "/test/jest/mocks/styleMock.js" 19 | }, 20 | "unmockedModulePathPatterns": [ 21 | "node_modules/react/", 22 | "node_modules/enzyme/" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.steamer/steamer-react-component.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | files: [ 3 | "src", 4 | "tools", 5 | "config", 6 | "README.md", 7 | ".eslintrc.js", 8 | ".eslintignore", 9 | ".stylelintrc.js", 10 | "postcss.config.js", 11 | "jest.conf.json", 12 | ".gitignore", 13 | ".babelrc", 14 | "example", 15 | ".editorconfig" 16 | ], 17 | options: [ 18 | { 19 | type: 'input', 20 | name: 'webserver', 21 | message: 'html url(//localhost:9000/)', 22 | default: "//localhost:9000/", 23 | }, 24 | { 25 | type: 'input', 26 | name: 'port', 27 | message: 'development server port(9000)', 28 | default: '9000', 29 | } 30 | ] 31 | }; 32 | -------------------------------------------------------------------------------- /tools/rules/fileRules.js: -------------------------------------------------------------------------------- 1 | `use strict`; 2 | 3 | 4 | 5 | module.exports = function(config) { 6 | 7 | var configWebpack = config.webpack, 8 | isProduction = config.env === 'production'; 9 | 10 | var rules = [ 11 | { 12 | test: /\.ico$/, 13 | loader: 'url-loader', 14 | options: { 15 | name: '[name].[ext]' 16 | } 17 | }, 18 | { 19 | test: /\.(jpe?g|png|gif|svg)$/i, 20 | loaders: [ 21 | { 22 | loader: 'url-loader', 23 | options: { 24 | publicPath: isProduction ? configWebpack.imgCdn : configWebpack.webserver, 25 | limit: 1000, 26 | name: 'img/[path]/[name].[ext]' 27 | } 28 | } 29 | ] 30 | } 31 | ]; 32 | 33 | return rules; 34 | }; -------------------------------------------------------------------------------- /tools/plugins/basePlugins.js: -------------------------------------------------------------------------------- 1 | `use strict`; 2 | 3 | const path = require('path'), 4 | os = require('os'); 5 | 6 | var UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 7 | 8 | module.exports = function(config, webpack) { 9 | 10 | var configWebpack = config.webpack, 11 | isProduction = config.env === 'production'; 12 | 13 | var plugins = [ 14 | new webpack.NoEmitOnErrorsPlugin(), 15 | new webpack.DefinePlugin(configWebpack.injectVar), 16 | // new webpack.optimize.ModuleConcatenationPlugin() 17 | ]; 18 | 19 | if (isProduction) { 20 | 21 | if (configWebpack.compress) { 22 | plugins.push(new UglifyJSPlugin({ 23 | parallel: { 24 | cache: true, 25 | workers: os.cpus().length, 26 | }, 27 | warnings: true, 28 | })); 29 | } 30 | } 31 | else { 32 | plugins.push(new webpack.HotModuleReplacementPlugin()); 33 | } 34 | 35 | return plugins; 36 | }; -------------------------------------------------------------------------------- /tools/optimization/index.js: -------------------------------------------------------------------------------- 1 | let UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 2 | let OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 3 | 4 | module.exports = function(config, webpack) { 5 | let configWebpack = config.webpack; 6 | let optimization = { 7 | minimizer: [] 8 | }; 9 | 10 | if (configWebpack.compress) { 11 | optimization.minimizer.push(new UglifyJsPlugin({ 12 | cache: true, 13 | parallel: true, 14 | sourceMap: true // set to true if you want JS source maps 15 | })); 16 | optimization.minimizer.push( 17 | new OptimizeCSSAssetsPlugin({ 18 | cssProcessor: require('cssnano'), 19 | cssProcessorOptions: { 20 | reduceIdents: false, 21 | autoprefixer: false, 22 | }, 23 | }) 24 | ); 25 | } 26 | 27 | return optimization; 28 | }; 29 | -------------------------------------------------------------------------------- /tools/rules/htmlRules.js: -------------------------------------------------------------------------------- 1 | `use strict`; 2 | 3 | 4 | 5 | module.exports = function(config) { 6 | 7 | var configWebpack = config.webpack; 8 | 9 | // 模板loader 10 | const templateRules = { 11 | html: { 12 | test: /\.html$/, 13 | loader: 'html-loader' 14 | }, 15 | pug: { 16 | test: /\.pug$/, 17 | loader: 'pug-loader' 18 | }, 19 | handlebars: { 20 | test: /\.handlebars$/, 21 | loader: 'handlebars-loader' 22 | }, 23 | ejs: { 24 | test: /\.ejs$/, 25 | loader: 'ejs-compiled-loader', 26 | query: { 27 | 'htmlmin': true, // or enable here 28 | 'htmlminOptions': { 29 | removeComments: true 30 | } 31 | } 32 | } 33 | }; 34 | 35 | var rules = []; 36 | 37 | configWebpack.template.forEach((tpl) => { 38 | let rule = templateRules[tpl] || ''; 39 | rule && rules.push(rule); 40 | }); 41 | 42 | return rules; 43 | }; -------------------------------------------------------------------------------- /tools/rules/jsRules.js: -------------------------------------------------------------------------------- 1 | `use strict`; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = function(config) { 6 | 7 | var isProduction = config.env === 'production'; 8 | var configWebpack = config.webpack; 9 | 10 | // js方言 11 | const jsRules = {}; 12 | 13 | var rules = [ 14 | { 15 | test: /\.js$/, 16 | loader: 'happypack/loader?id=1', 17 | exclude: /node_modules/ 18 | } 19 | ]; 20 | 21 | if (!isProduction) { 22 | // 为了统计代码覆盖率,对 js 文件加入 istanbul-instrumenter-loader 23 | rules.push({ 24 | test: /\.(js)$/, 25 | loader: 'istanbul-instrumenter-loader', 26 | exclude: /node_modules/, 27 | include: /src|packages/, 28 | enforce: 'post', 29 | options: { 30 | esModules: true 31 | } 32 | }); 33 | } 34 | 35 | configWebpack.js.forEach((tpl) => { 36 | let rule = jsRules[tpl] || ''; 37 | rule && rules.push(rule); 38 | }); 39 | 40 | return rules; 41 | }; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 steamerjs 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 | # steamer-react-component 2 | 3 | 用于开发 `react` 组件的脚手架 4 | 5 | 6 | ## 快速启动 7 | 8 | * 推荐 >> 使用[steamerjs](https://steamerjs.github.io/docs/projectkits/Bootstrap.html)安装 9 | 10 | ```javascript 11 | 12 | npm i -g steamerjs steamer-plugin-kit 13 | 14 | npm i -g steamer-react-component 15 | 16 | steamer kit 17 | ``` 18 | 19 | * 或直接从github clone 下来 20 | 21 | ### 安装依赖 22 | ```bash 23 | npm i 24 | ``` 25 | 26 | ### 开发 27 | 28 | ```bash 29 | npm run start 或 npm run dev 30 | 31 | // 打开链接,查看 demo 32 | localhost:9000 33 | ``` 34 | 35 | > ps:此处在开启开发模式下默认会自动打开浏览器。 36 | 37 | ### 代码规范扫描 38 | 39 | ```bash 40 | npm run lint 41 | ``` 42 | 43 | > ps:此处的扫描在开发模式下是默认开启的,不需要手动执行lint。 44 | 45 | ### 测试 46 | 47 | ```bash 48 | // 使用 jest 测试 49 | npm run test 或 npm run jest 50 | 51 | // 使用 karma 测试 52 | npm run karma 53 | ``` 54 | 55 | ### 生产代码生成 56 | 57 | ```bash 58 | // 使用 babel 编译 59 | npm run dist 或 npm run babel 60 | 61 | // 使用 webapck 编译 62 | npm run webpack 63 | ``` 64 | 65 | > ps:关于两种测试方式和两种编译方式的区别可查看[这里](https://steamerjs.github.io/docs/Componet-Standard.html#两种编译与两种测试方式)。 66 | 67 | ## 编写demo 68 | 69 | 直接在`/example/src/container/`目录下调整关于组件的使用逻辑和范例样式即可,如范例中该目录下的index.js和index.less文件。 70 | 71 | 72 | ## 脚手架文档 73 | [参见文档 - 组件脚手架](https://steamerjs.github.io/docs/componentkits/Starterkit.html) 74 | 75 | ## 文章参考 76 | * [从工程化角度讨论如何快速构建可靠React组件](https://github.com/lcxfs1991/blog/issues/18) 77 | * [聊一聊前端自动化测试](https://github.com/tmallfe/tmallfe.github.io/issues/37) 78 | -------------------------------------------------------------------------------- /tools/script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('steamer-webpack-utils'), 4 | webpack = require('webpack'), 5 | opn = require('opn'), 6 | fs = require('fs'), 7 | path = require('path'), 8 | karmaServer = require('karma').Server; 9 | 10 | var isProduction = process.env.NODE_ENV === 'production', 11 | isKarma = process.env.KARMA_ENV === 'karma'; 12 | 13 | const feature = require('./feature/feature'); 14 | 15 | if (feature.installDependency()) { 16 | return; 17 | } 18 | 19 | 20 | if (isKarma) { 21 | let config = require('../config/project'), 22 | configWebpack = config.webpack; 23 | 24 | let server = new karmaServer({ 25 | configFile: path.join(configWebpack.path.test, '/unit/karma.conf.js'), 26 | singleRun: true 27 | }, function() { 28 | console.log('karma test done!'); 29 | opn(path.join(configWebpack.path.test, 'unit/coverage/lcov-report/index.html')); 30 | }); 31 | server.start(); 32 | } 33 | else if (!isProduction) { 34 | require('./server'); 35 | } 36 | else if (isProduction) { 37 | compilerRun(require('./webpack.base')); 38 | } 39 | 40 | function compilerRun(config) { 41 | var compiler = webpack(config); 42 | 43 | compiler.run(function(err, stats) { 44 | if (!err) { 45 | // const jsonStats = stats.toJson(); 46 | // print asset stats 47 | // fs.writeFileSync("stats.txt", JSON.stringify(jsonStats, " " , 4)) 48 | 49 | console.log(stats.toString({ 50 | assets: true, 51 | cached: true, 52 | colors: true, 53 | children: false, 54 | errors: true, 55 | warnings: true, 56 | version: true, 57 | })); 58 | } 59 | else { 60 | console.log(err); 61 | } 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /src/pindex.js: -------------------------------------------------------------------------------- 1 | /** @jsx h */ 2 | import Preact, { h, Component } from 'preact'; 3 | import Spin from './spin'; 4 | 5 | import './index.less'; 6 | 7 | export default class Spinner extends Component { 8 | constructor(props, context) { 9 | super(props, context); 10 | this.state = { 11 | 12 | }; 13 | } 14 | 15 | componentWillMount() { 16 | 17 | } 18 | 19 | componentDidMount() { 20 | var opts = { 21 | lines: 12, // The number of lines to draw 22 | length: 3, // The length of each line 23 | width: 2, // The line thickness 24 | radius: 6, // The radius of the inner circle 25 | scale: 1.0, // Scales overall size of the spinner 26 | corners: 1, // Roundness (0..1) 27 | color: '#777', // #rgb or #rrggbb 28 | opacity: 1 / 4, // Opacity of the lines 29 | rotate: 0, // Rotation offset 30 | direction: 1, // 1: clockwise, -1: counterclockwise 31 | speed: 1, // Rounds per second 32 | trail: 100, // Afterglow percentage 33 | fps: 20, // Frames per second when using setTimeout() 34 | zIndex: 2e9, // Use a high z-index by default 35 | className: 'spin', // CSS class to assign to the element 36 | top: '50%', // center vertically 37 | left: '50%', // center horizontally 38 | shadow: false, // Whether to render a shadow 39 | hwaccel: false, // Whether to use hardware acceleration (might be buggy) 40 | position: 'absolute' // Element positioning 41 | }; 42 | var target = document.getElementById('spin'); 43 | var spinner = new Spin(opts).spin(target); 44 | } 45 | 46 | render() { 47 | console.log('render spinner'); 48 | 49 | var isShow = this.props.isShow || false; 50 | var spinStyle = { 51 | display: (isShow) ? 'block' : 'none' 52 | }; 53 | 54 | return ( 55 |
56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tools/server.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | express = require('express'), 3 | app = express(), 4 | webpack = require('webpack'), 5 | webpackDevMiddleware = require("webpack-dev-middleware"), 6 | webpackHotMiddleware = require("webpack-hot-middleware"), 7 | proxy = require('http-proxy-middleware'); 8 | 9 | var webpackConfig = require("./webpack.base.js"), 10 | config = require("../config/project"), 11 | configWebpack = config.webpack, 12 | port = configWebpack.port, 13 | route = Array.isArray(configWebpack.route) ? configWebpack.route : [configWebpack.route]; 14 | 15 | function addProtocal(urlString) { 16 | if (!!~urlString.indexOf('http:') || !!~urlString.indexOf('https:')) { 17 | return urlString; 18 | } 19 | return 'http:' + urlString; 20 | } 21 | 22 | var urlObject = url.parse(addProtocal(configWebpack.webserver)); 23 | 24 | for (var key in webpackConfig.entry) { 25 | webpackConfig.entry[key].unshift(`webpack-hot-middleware/client?reload=true&dynamicPublicPath=true&path=__webpack_hmr`); 26 | webpackConfig.entry[key].unshift('react-hot-loader/patch'); 27 | } 28 | 29 | var compiler = webpack(webpackConfig); 30 | app.use(webpackDevMiddleware(compiler, { 31 | noInfo: true, 32 | stats: { 33 | colors: true 34 | }, 35 | publicPath: configWebpack.webserver 36 | })); 37 | 38 | app.use(webpackHotMiddleware(compiler, { 39 | // 这里和上面的client配合,可以修正 webpack_hmr 的路径为项目路径的子路径,而不是直接成为 host 子路径(从publicPath开始,而不是根开始) 40 | // https://github.com/glenjamin/webpack-hot-middleware/issues/24 41 | path: `${urlObject.path}__webpack_hmr` 42 | })); 43 | 44 | // 静态资源转发 45 | route.forEach((rt) => { 46 | app.use(rt, proxy({target: `http://127.0.0.1:${port}`, pathRewrite: {[`^${rt}`] : '/'}})); 47 | }); 48 | 49 | app.listen(port, function(err) { 50 | if (err) { 51 | console.error(err); 52 | } 53 | else { 54 | console.info("Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port); 55 | } 56 | }); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Spin from './spin'; 3 | 4 | import './index.less'; 5 | 6 | export default class Spinner extends Component { 7 | constructor(props, context) { 8 | super(props, context); 9 | this.state = { 10 | 11 | }; 12 | } 13 | 14 | componentWillMount() { 15 | 16 | } 17 | 18 | componentDidMount() { 19 | let opts = { 20 | lines: 12, // The number of lines to draw 21 | length: 3, // The length of each line 22 | width: 2, // The line thickness 23 | radius: 6, // The radius of the inner circle 24 | scale: 1.0, // Scales overall size of the spinner 25 | corners: 1, // Roundness (0..1) 26 | color: '#777', // #rgb or #rrggbb 27 | opacity: 1 / 4, // Opacity of the lines 28 | rotate: 0, // Rotation offset 29 | direction: 1, // 1: clockwise, -1: counterclockwise 30 | speed: 1, // Rounds per second 31 | trail: 100, // Afterglow percentage 32 | fps: 20, // Frames per second when using setTimeout() 33 | zIndex: 2e9, // Use a high z-index by default 34 | className: 'spin', // CSS class to assign to the element 35 | top: '50%', // center vertically 36 | left: '50%', // center horizontally 37 | shadow: false, // Whether to render a shadow 38 | hwaccel: false, // Whether to use hardware acceleration (might be buggy) 39 | position: 'absolute' // Element positioning 40 | }; 41 | let target = document.getElementById('spin'); 42 | let spinner = new Spin(opts).spin(target); 43 | } 44 | 45 | render() { 46 | let isShow = this.props.isShow || false; 47 | let spinStyle = { 48 | display: (isShow) ? 'block' : 'none' 49 | }; 50 | 51 | return ( 52 |
53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tools/plugins/resourcePlugins.js: -------------------------------------------------------------------------------- 1 | `use strict`; 2 | 3 | const path = require('path'); 4 | 5 | let Clean = require('clean-webpack-plugin'); 6 | let WriteFilePlugin = require('write-file-webpack-plugin'); 7 | let HappyPack = require('happypack'); 8 | let HtmlResWebpackPlugin = require('html-res-webpack-plugin'); 9 | let MiniCssExtractPlugin = require('mini-css-extract-plugin'); 10 | 11 | module.exports = function(config, webpack) { 12 | 13 | var configWebpack = config.webpack, 14 | isProduction = config.env === 'production'; 15 | 16 | var plugins = [ 17 | new HappyPack({ 18 | id: '1', 19 | verbose: false, 20 | loaders: [{ 21 | path: 'babel-loader', 22 | options: { 23 | cacheDirectory: './.cache/' 24 | } 25 | }] 26 | }), 27 | new MiniCssExtractPlugin({ 28 | filename: `css/[name].css`, 29 | chunkFilename: 'css/[name].css' 30 | }), 31 | 32 | ]; 33 | 34 | if (isProduction) { 35 | 36 | } 37 | else { 38 | if (configWebpack.showSource) { 39 | plugins.push(new WriteFilePlugin()); 40 | } 41 | 42 | config.webpack.html.forEach(function(page, key) { 43 | plugins.push(new HtmlResWebpackPlugin({ 44 | mode: "html", 45 | filename: page.key + ".html", 46 | template: page.path, 47 | htmlMinify: null, 48 | entryLog: true, 49 | removeUnMatchedAssets: true, 50 | env: isProduction ? 'production' : 'development', 51 | templateContent: function(tpl) { 52 | return tpl; 53 | } 54 | })); 55 | }); 56 | } 57 | 58 | if (configWebpack.clean) { 59 | // 生产环境,只删除一次 60 | plugins.push(new Clean([isProduction ? configWebpack.path.dist : path.join(configWebpack.path.example, 'dev')], {root: path.resolve()}),); 61 | } 62 | 63 | return plugins; 64 | }; 65 | -------------------------------------------------------------------------------- /tools/rules/styleRules.js: -------------------------------------------------------------------------------- 1 | `use strict`; 2 | 3 | const path = require('path'); 4 | const merge = require('lodash.merge'); 5 | 6 | let MiniCssExtractPlugin = require('mini-css-extract-plugin'); 7 | 8 | module.exports = function(config) { 9 | 10 | let configWebpack = config.webpack; 11 | let isProduction = config.env === 'production'; 12 | 13 | 14 | let includePaths = [ 15 | path.resolve('node_modules'), 16 | config.webpack.path.src 17 | ]; 18 | 19 | // 样式loader 20 | let commonLoaders = [ 21 | { 22 | loader: 'cache-loader', 23 | options: { 24 | // provide a cache directory where cache items should be stored 25 | cacheDirectory: path.resolve('.cache') 26 | } 27 | }, 28 | { 29 | loader: 'css-loader', 30 | options: { 31 | localIdentName: '[name]-[local]-[hash:base64:5]', 32 | modules: config.webpack.cssModule, 33 | autoprefixer: true, 34 | minimize: true, 35 | importLoaders: 2 36 | } 37 | }, 38 | { 39 | loader: 'postcss-loader' 40 | } 41 | ]; 42 | 43 | if (isProduction) { 44 | commonLoaders.splice(0, 0, { loader: MiniCssExtractPlugin.loader }); 45 | } 46 | else { 47 | commonLoaders.splice(0, 0, { loader: 'style-loader' }); 48 | } 49 | 50 | 51 | const styleRules = { 52 | css: { 53 | test: /\.css$/, 54 | use: commonLoaders, 55 | }, 56 | less: { 57 | test: /\.less$/, 58 | use: merge([], commonLoaders).concat([{ 59 | loader: 'less-loader', 60 | options: { 61 | javascriptEnabled: true 62 | // paths: includePaths 63 | } 64 | }]), 65 | }, 66 | stylus: { 67 | test: /\.styl$/, 68 | use: merge([], commonLoaders).concat([{ 69 | loader: 'stylus-loader' 70 | }]), 71 | }, 72 | sass: { 73 | test: /\.s(a|c)ss$/, 74 | use: merge([], commonLoaders).concat([{ 75 | loader: 'sass-loader', 76 | options: { 77 | } 78 | }]), 79 | } 80 | }; 81 | 82 | let rules = []; 83 | 84 | configWebpack.style.forEach((styleParam) => { 85 | let style = (styleParam === 'scss') ? 'sass' : styleParam; 86 | let rule = styleRules[style] || ''; 87 | rule && rules.push(rule); 88 | }); 89 | 90 | return rules; 91 | }; 92 | -------------------------------------------------------------------------------- /tools/webpack.base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'), 4 | fs = require('fs'), 5 | os = require('os'), 6 | utils = require('steamer-webpack-utils'), 7 | webpack = require('webpack'), 8 | webpackMerge = require('webpack-merge'); 9 | 10 | var config = require('../config/project'), 11 | configWebpack = config.webpack, 12 | configWebpackMerge = config.webpackMerge, 13 | configCustom = config.custom, 14 | isProduction = config.env === 'production'; 15 | 16 | var baseConfig = { 17 | mode: isProduction ? 'production' : 'development', 18 | context: configWebpack.path.src, 19 | entry: configWebpack.entry, 20 | output: { 21 | publicPath: isProduction ? `/${path.basename(configWebpack.path.dist)}/` : configWebpack.webserver, 22 | path: isProduction ? configWebpack.path.dist : path.join(configWebpack.path.example, 'dev'), 23 | filename: '[name].js', 24 | }, 25 | module: { 26 | rules: [] 27 | }, 28 | resolve: { 29 | modules: [ 30 | configWebpack.path.src, 31 | 'node_modules', 32 | path.join(configWebpack.path.src, 'css/sprites') 33 | ], 34 | extensions: [ 35 | '.ts', '.tsx', '.js', '.jsx', '.css', '.scss', 'sass', '.less', '.styl', 36 | '.png', '.jpg', '.jpeg', '.ico', '.ejs', '.pug', '.handlebars', '.swf', '.vue' 37 | ], 38 | alias: {} 39 | }, 40 | plugins: [], 41 | optimization: {}, 42 | watch: !isProduction, 43 | performance: { 44 | hints: isProduction ? 'warning' : false, 45 | assetFilter: function(assetFilename) { 46 | return assetFilename.endsWith('.js') || assetFilename.endsWith('.css'); 47 | } 48 | } 49 | }; 50 | 51 | /************* 处理脚手架基础rules & plugins *************/ 52 | var rules = fs.readdirSync(path.join(__dirname, 'rules')), 53 | plugins = fs.readdirSync(path.join(__dirname, 'plugins')); 54 | 55 | var baseConfigRules = [], 56 | baseConfigPlugins = []; 57 | 58 | rules.forEach((rule) => { 59 | baseConfigRules = baseConfigRules.concat(require(`./rules/${rule}`)(config)); 60 | }); 61 | 62 | plugins.forEach((plugin) => { 63 | baseConfigPlugins = baseConfigPlugins.concat(require(`./plugins/${plugin}`)(config, webpack)); 64 | }); 65 | 66 | baseConfig.module.rules = baseConfigRules; 67 | baseConfig.plugins = baseConfigPlugins; 68 | baseConfig.optimization = require('./optimization')(config, webpack); 69 | 70 | // console.log(rules, plugins); 71 | 72 | /************* base 与 user config 合并 *************/ 73 | var userConfig = { 74 | output: configCustom.getOutput(), 75 | module: configCustom.getModule(), 76 | resolve: configCustom.getResolve(), 77 | externals: configCustom.getExternals(), 78 | plugins: configCustom.getPlugins() 79 | }; 80 | 81 | var otherConfig = configCustom.getOtherOptions(); 82 | 83 | for (let key in otherConfig) { 84 | userConfig[key] = otherConfig[key]; 85 | } 86 | 87 | baseConfig = configWebpackMerge.mergeProcess(baseConfig); 88 | 89 | var webpackConfig = webpackMerge.smartStrategy( 90 | configWebpackMerge.smartStrategyOption 91 | )(baseConfig, userConfig); 92 | 93 | // console.log(JSON.stringify(webpackConfig, null, 4)); 94 | 95 | module.exports = webpackConfig; 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steamer-react-component", 3 | "version": "3.0.1", 4 | "description": "react component development starter kit", 5 | "main": "./.steamer/steamer-react-component.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=development node ./tools/script.js", 8 | "dev": "npm run start", 9 | "dist": "npm run babel", 10 | "test": "npm run karma", 11 | "babel": "babel src --out-dir dist --copy-files", 12 | "webpack": "cross-env NODE_ENV=production node ./tools/script.js", 13 | "jest": "jest --coverage --config ./jest.conf.json", 14 | "karma": "cross-env BABEL_ENV=test NODE_ENV=production KARMA_ENV=karma node ./tools/script.js", 15 | "lint": "eslint src/**/*.js && stylelint src/**/*.less", 16 | "lint:js": "eslint src/**/*.js", 17 | "lint:style": "stylelint src/**/*.less" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/steamerjs/steamer-react-component.git" 22 | }, 23 | "keywords": [ 24 | "steamer starterkit", 25 | "steamer", 26 | "react", 27 | "component", 28 | "starter kit", 29 | "boilerplate" 30 | ], 31 | "author": "steamerjs", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/steamerjs/steamer-react-component/issues" 35 | }, 36 | "homepage": "https://github.com/steamerjs/steamer-react-component#readme", 37 | "dependencies": {}, 38 | "devDependencies": { 39 | "@alloyteam/stylelint-config-standard": "0.0.2", 40 | "autoprefixer": "^9.1.5", 41 | "babel-cli": "6.26.0", 42 | "babel-core": "6.26.0", 43 | "babel-istanbul": "^0.12.2", 44 | "babel-jest": "^21.2.0", 45 | "babel-loader": "^7.1.2", 46 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 47 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 48 | "babel-preset-airbnb": "^2.4.0", 49 | "babel-preset-env": "^1.6.1", 50 | "babel-preset-es2015": "6.24.1", 51 | "babel-preset-react": "6.24.1", 52 | "cache-loader": "^1.2.2", 53 | "chai": "^4.2.0", 54 | "clean-webpack-plugin": "^0.1.19", 55 | "cross-env": "^5.2.0", 56 | "css-loader": "^1.0.0", 57 | "enzyme": "^3.7.0", 58 | "enzyme-adapter-react-15": "^1.1.1", 59 | "eslint": "^5.6.1", 60 | "eslint-config-alloy": "^1.4.2", 61 | "express": "^4.16.3", 62 | "file-loader": "^2.0.0", 63 | "happypack": "^5.0.0", 64 | "html-loader": "^0.5.5", 65 | "html-res-webpack-plugin": "^4.0.4", 66 | "http-proxy-middleware": "^0.19.0", 67 | "identity-obj-proxy": "^3.0.0", 68 | "istanbul-instrumenter-loader": "^3.0.1", 69 | "jest": "^23.6.0", 70 | "jest-environment-jsdom": "^23.4.0", 71 | "jsdom": "^12.2.0", 72 | "karma": "^3.0.0", 73 | "karma-chai": "^0.1.0", 74 | "karma-chrome-launcher": "^2.2.0", 75 | "karma-coverage-istanbul-reporter": "^2.0.4", 76 | "karma-mocha": "^1.3.0", 77 | "karma-phantomjs-launcher": "^1.0.4", 78 | "karma-sinon-chai": "^2.0.2", 79 | "karma-sourcemap-loader": "^0.3.7", 80 | "karma-spec-reporter": "0.0.32", 81 | "karma-webpack": "^3.0.5", 82 | "less": "^3.8.1", 83 | "less-loader": "^4.1.0", 84 | "lodash.merge": "^4.6.1", 85 | "mini-css-extract-plugin": "^0.4.4", 86 | "mocha": "^5.2.0", 87 | "opn": "^5.4.0", 88 | "optimize-css-assets-webpack-plugin": "^5.0.1", 89 | "postcss-import": "^12.0.0", 90 | "postcss-loader": "^3.0.0", 91 | "preact": "^8.3.1", 92 | "react": "^15.6.2", 93 | "react-addons-test-utils": "^15.6.2", 94 | "react-dom": "^15.6.2", 95 | "react-hot-loader": "^3.1.1", 96 | "react-test-renderer": "^15.6.2", 97 | "regenerator-runtime": "^0.12.1", 98 | "sinon": "^6.3.5", 99 | "sinon-chai": "^3.2.0", 100 | "steamer-webpack-utils": "^1.1.1", 101 | "style-loader": "^0.23.1", 102 | "stylelint": "^9.6.0", 103 | "uglifyjs-webpack-plugin": "^2.0.1", 104 | "url-loader": "^1.1.2", 105 | "webpack": "^4.20.2", 106 | "webpack-dev-middleware": "^3.4.0", 107 | "webpack-hot-middleware": "^2.24.3", 108 | "webpack-merge": "^4.1.4", 109 | "write-file-webpack-plugin": "^4.4.1" 110 | }, 111 | "engines": { 112 | "node": ">=5.0.0" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tools/feature/feature.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // used for install dependencies and files to support certain kinds of features 4 | 5 | var path = require('path'), 6 | project = require('../../config/project'), 7 | pkgJson = require('../../package.json'), 8 | merge = require('lodash.merge'), 9 | spawnSync = require('child_process').spawnSync, 10 | utils = require('steamer-webpack-utils'); 11 | 12 | var dependency = { 13 | template: { 14 | html: { 15 | 'html-loader': '^0.4.5' 16 | }, 17 | handlebars: { 18 | 'handlebars-loader': '^1.5.0', 19 | 'handlebars': '^4.0.10' 20 | }, 21 | pug: { 22 | 'pug-loader': '^2.3.0', 23 | 'pug': '^2.0.0-rc.2' 24 | }, 25 | ejs: { 26 | 'ejs-compiled-loader': '^1.1.0', 27 | 'ejs': '^2.5.6' 28 | } 29 | 30 | }, 31 | style: { 32 | css: { 33 | 'style-loader': '^0.18.2', 34 | 'css-loader': '^0.28.4' 35 | }, 36 | less: { 37 | 'style-loader': '^0.18.2', 38 | 'css-loader': '^0.28.4', 39 | 'less': '^2.7.2', 40 | 'less-loader': '^4.0.4' 41 | }, 42 | sass: { 43 | 'style-loader': '^0.18.2', 44 | 'css-loader': '^0.28.4', 45 | 'node-sass': '^4.5.3', 46 | 'sass-loader': '^6.0.6' 47 | }, 48 | scss: { 49 | 'style-loader': '^0.18.2', 50 | 'css-loader': '^0.28.4', 51 | 'node-sass': '^4.5.3', 52 | 'sass-loader': '^6.0.6' 53 | }, 54 | stylus: { 55 | 'style-loader': '^0.18.2', 56 | 'css-loader': '^0.28.4', 57 | 'stylus': '^0.54.5', 58 | 'stylus-loader': '^3.0.1' 59 | } 60 | }, 61 | js: { 62 | ts: { 63 | 'awesome-typescript-loader': '^3.2.1', 64 | 'typescript': '^2.4.1', 65 | '@types/react': '^15.0.35', 66 | '@types/react-dom': '^15.5.1' 67 | } 68 | } 69 | }; 70 | 71 | var files = { 72 | template: {}, 73 | style: {}, 74 | js: { 75 | ts: [ 76 | { 77 | src: path.join(__dirname, './tsconfig.json'), 78 | dist: path.resolve('tsconfig.json') 79 | } 80 | ] 81 | } 82 | }; 83 | 84 | module.exports = { 85 | installDependency: function() { 86 | console.log(); 87 | 88 | var dependencies = merge({}, pkgJson.dependencies, pkgJson.devDependencies); 89 | 90 | var installDep = {}, 91 | installFile = { 92 | template: {}, 93 | style: {}, 94 | js: {} 95 | }, 96 | cmd = ''; 97 | 98 | project.webpack.template.forEach((item1) => { 99 | let dep = dependency['template'][item1] || {}; 100 | 101 | Object.keys(dep).forEach((item2) => { 102 | if (!dependencies[item2]) { 103 | installDep[item2] = dependency['template'][item1][item2]; 104 | installFile.template[item1] = true; 105 | } 106 | }); 107 | }); 108 | 109 | project.webpack.style.forEach((item1) => { 110 | let dep = dependency['style'][item1] || {}; 111 | 112 | Object.keys(dep).forEach((item2) => { 113 | if (!dependencies[item2]) { 114 | installDep[item2] = dependency['style'][item1][item2]; 115 | installFile.style[item1] = true; 116 | } 117 | }); 118 | }); 119 | 120 | project.webpack.js.forEach((item1) => { 121 | let dep = dependency['js'][item1] || {}; 122 | 123 | Object.keys(dep).forEach((item2) => { 124 | if (!dependencies[item2]) { 125 | installDep[item2] = dependency['js'][item1][item2]; 126 | installFile.js[item1] = true; 127 | } 128 | }); 129 | }); 130 | 131 | Object.keys(installDep).forEach((item) => { 132 | cmd += (item + '@' + installDep[item] + ' '); 133 | }); 134 | 135 | if (cmd) { 136 | utils.info('Start installing missing dependencies. Please wait......'); 137 | this.copyFile(installFile); 138 | spawnSync(project.npm, ['install', '--save-dev', cmd], { stdio: 'inherit', shell: true }); 139 | utils.info('Dependencies installation complete. Please run your command again.'); 140 | return true; 141 | } 142 | else { 143 | return false; 144 | } 145 | }, 146 | copyFile: function(installFile) { 147 | Object.keys(installFile.template).forEach((item1) => { 148 | let fileArr = files.template[item1] || []; 149 | fileArr.forEach((item2) => { 150 | utils.info('file ' + item2.src + ' is copyied to ' + item2.dist); 151 | utils.fs.copySync(item2.src, item2.dist); 152 | }); 153 | }); 154 | 155 | Object.keys(installFile.style).forEach((item1) => { 156 | let fileArr = files.style[item1] || []; 157 | fileArr.forEach((item2) => { 158 | utils.info('file ' + item2.src + ' is copyied to ' + item2.dist); 159 | utils.fs.copySync(item2.src, item2.dist); 160 | }); 161 | }); 162 | Object.keys(installFile.js).forEach((item1) => { 163 | let fileArr = files.js[item1] || []; 164 | fileArr.forEach((item2) => { 165 | utils.info('file ' + item2.src + ' is copyied to ' + item2.dist); 166 | utils.fs.copySync(item2.src, item2.dist); 167 | }); 168 | }); 169 | } 170 | }; -------------------------------------------------------------------------------- /config/project.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'), 4 | os = require('os'), 5 | webpack = require('webpack'), 6 | utils = require('steamer-webpack-utils'), 7 | steamerConfig = require('./steamer.config'), 8 | __basename = path.dirname(__dirname), 9 | __env = process.env.NODE_ENV, 10 | isProduction = __env === 'production'; 11 | 12 | var srcPath = path.resolve(__basename, "src"), 13 | devPath = path.resolve(__basename, "dev"), 14 | distPath = path.resolve(__basename, "dist"), 15 | examplePath = path.resolve(__basename, "example"), 16 | testPath = path.resolve(__basename, "test"); 17 | 18 | var entry = {}; 19 | 20 | 21 | if (isProduction) { 22 | entry = { 23 | index: path.join(srcPath, 'index.js'), 24 | pindex: path.join(srcPath, 'pindex.js') 25 | }; 26 | } 27 | else { 28 | // 根据约定,自动扫描js entry,约定是example/src/page/xxx/main.js 或 example/src/page/xxx/main.jsx 29 | /** 30 | 获取结果示例 31 | { 32 | 'js/index': [path.join(configWebpack.path.src, "/page/index/main.js")], 33 | 'js/spa': [path.join(configWebpack.path.src, "/page/spa/main.js")], 34 | 'js/pindex': [path.join(configWebpack.path.src, "/page/pindex/main.jsx")], 35 | } 36 | */ 37 | entry = utils.filterJsFileByCmd(utils.getJsEntry({ 38 | srcPath: path.join(examplePath, "src/page"), 39 | fileName: "main", 40 | extensions: ["js", "jsx"], 41 | keyPrefix: "", 42 | level: 1 43 | })); 44 | } 45 | 46 | // ========================= webpack快捷配置 ========================= 47 | // 基本情况下,你只需要关注这里的配置 48 | var config = { 49 | // ========================= webpack环境配置 ========================= 50 | env: __env, 51 | 52 | // 默认使用的npm命令行 53 | npm: 'npm', 54 | 55 | webpack: { 56 | 57 | // ========================= webpack路径与url ========================= 58 | // 项目路径 59 | path: { 60 | src: srcPath, 61 | dev: devPath, 62 | dist: distPath, 63 | example: examplePath, 64 | test: testPath, 65 | }, 66 | 67 | // ========================= webpack服务器及路由配置 ========================= 68 | // 开发服务器配置 69 | webserver: steamerConfig.webserver, 70 | port: steamerConfig.port, // port for local server 71 | route: [], // proxy route, 例如: /news/ 72 | 73 | // ========================= webpack自定义配置 ========================= 74 | // 是否显示开发环境下的生成文件 75 | showSource: true, 76 | 77 | // 是否清理生成文件夹 78 | clean: true, 79 | 80 | // 是否压缩 81 | compress: false, 82 | 83 | // javascript 方言, 目前仅支持 ts(typescript) 84 | js: [], 85 | 86 | // 预编译器,默认支持css 和 less. sass, scss 和 stylus 由npm-install-webpack-plugin自动安装 87 | style: [ 88 | "css", "less" 89 | ], 90 | // 生产环境是否提取css 91 | extractCss: true, 92 | // 是否启用css模块化 93 | cssModule: false, 94 | 95 | // html 模板. 默认支持html 和 ejs, handlebars 和 pug 由npm-install-webpack-plugin自动安装 96 | template: [ 97 | "html", 98 | ], 99 | 100 | // 利用DefinePlugin给应用注入变量 101 | injectVar: { 102 | "process.env": { 103 | NODE_ENV: JSON.stringify(__env) 104 | } 105 | }, 106 | 107 | alias: { 108 | 109 | }, 110 | 111 | // ========================= webpack entry配置 ========================= 112 | entry: entry, 113 | 114 | // 自动扫描html,配合html-res-webpack-plugin 115 | /** 116 | 获取结果示例 117 | [ 118 | { 119 | key: 'index', 120 | path: 'path/src/page/index/index.html' 121 | }, 122 | { 123 | key: 'spa', 124 | path: 'path/src/page/spa/index.html' 125 | }, 126 | { 127 | key: 'pindex', 128 | path: 'path/src/page/pindex/index.html' 129 | } 130 | ] 131 | */ 132 | html: utils.filterHtmlFileByCmd(utils.getHtmlEntry({ 133 | srcPath: path.join(examplePath, "src/page"), 134 | level: 1 135 | })), 136 | 137 | }, 138 | }; 139 | 140 | 141 | 142 | // ========================= webpack深度配置 ========================= 143 | // 使用了webpack-merge与webpack.base.js进行配置合并 144 | // 如果上面的配置仍未能满足你,你可以在此处对webpack直接进行配置,这里的配置与webpack的配置项目一一对应 145 | config.custom = { 146 | // webpack output 147 | getOutput: function() { 148 | 149 | if (isProduction) { 150 | return { 151 | library: "lib", 152 | libraryTarget: "commonjs2", 153 | }; 154 | } 155 | else { 156 | return {}; 157 | } 158 | }, 159 | 160 | // webpack module 161 | getModule: function() { 162 | 163 | var module = { 164 | rules: [] 165 | }; 166 | 167 | return module; 168 | }, 169 | 170 | // webpack resolve 171 | getResolve: function() { 172 | return { 173 | alias: config.webpack.alias 174 | }; 175 | }, 176 | 177 | // webpack plugins 178 | getPlugins: function() { 179 | var plugins = []; 180 | 181 | return plugins; 182 | }, 183 | 184 | // webpack externals 185 | getExternals: function() { 186 | if (isProduction) { 187 | return { 188 | 'react': "React", 189 | 'react-dom': "ReactDOM", 190 | 'preact': 'preact', 191 | }; 192 | } 193 | 194 | return {}; 195 | }, 196 | 197 | // 其它 webpack 配置 198 | getOtherOptions: function() { 199 | return {}; 200 | } 201 | }; 202 | 203 | // ========================= webpack merge 的策略 ========================= 204 | config.webpackMerge = { 205 | // webpack-merge smartStrategy 配置 206 | smartStrategyOption: { 207 | "module.rules": "append", 208 | "plugins": "append" 209 | }, 210 | 211 | // 在smartStrategy merge 之前,用户可以先行对 webpack.base.js 的配置进行处理 212 | mergeProcess: function(webpackBaseConfig) { 213 | 214 | return webpackBaseConfig; 215 | } 216 | }; 217 | 218 | module.exports = config; 219 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | let webpackTestConf = require('../../tools/webpack.base.js'); 4 | 5 | // karma 目前只认 umd,commonjs/commonjs2会报错 6 | webpackTestConf.output.libraryTarget = 'umd'; 7 | delete webpackTestConf.output.library; 8 | // 需要删掉entry 9 | delete webpackTestConf.entry; 10 | // 不能设置 vue 的 externals 11 | delete webpackTestConf.externals; 12 | webpackTestConf.externals = { 13 | 'cheerio': 'window', 14 | 'react/addons': true, 15 | 'react/lib/ExecutionEnvironment': true, 16 | 'react/lib/ReactContext': true 17 | }; 18 | 19 | module.exports = function(config) { 20 | config.set({ 21 | // base path that will be used to resolve all patterns (eg. files, exclude) 22 | // basePath: '', 23 | // 手动引入 karma 的各项插件,如果不显式引入,karma 也会自动寻找 karma- 开头的插件并自动引入 24 | plugins: [ 25 | 'karma-coverage-istanbul-reporter', 26 | 'karma-mocha', 27 | 'karma-sinon-chai', 28 | 'karma-webpack', 29 | 'karma-sourcemap-loader', 30 | 'karma-spec-reporter', 31 | 'karma-phantomjs-launcher', 32 | 'karma-chrome-launcher' 33 | ], 34 | // frameworks to use 35 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 36 | // 设定要使用的 frameworks 37 | frameworks: ['mocha', 'sinon-chai'], 38 | // list of files / patterns to load in the browser 39 | // 入口文件,按照 istanbul-instrumenter-loader 的要求来写 40 | files: ['./index.js'], 41 | // list of files to exclude 42 | exclude: [ 43 | ], 44 | // preprocess matching files before serving them to the browser 45 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 46 | // 加入 webpack 与 sourcemap 插件 47 | preprocessors: { 48 | './index.js': ['webpack', 'sourcemap'], 49 | }, 50 | // test results reporter to use 51 | // possible values: 'dots', 'progress' 52 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 53 | // 设定报告输出插件: spec 和 coverage-istanbul 54 | reporters: ['spec', 'coverage-istanbul'], 55 | // coverage-istanbul 输出配置,报告文件输出于根目录下的 coverage 文件夹内 56 | coverageIstanbulReporter: { 57 | // reports can be any that are listed here: https://github.com/istanbuljs/istanbul-reports/tree/590e6b0089f67b723a1fdf57bc7ccc080ff189d7/lib 58 | reports: ['html', 'lcovonly', 'text-summary'], 59 | // base output directory 60 | dir: path.join(__dirname, './coverage'), 61 | // if using webpack and pre-loaders, work around webpack breaking the source path 62 | fixWebpackSourcePaths: true, 63 | // Most reporters accept additional config options. You can pass these through the `report-config` option 64 | 'report-config': { 65 | // all options available at: https://github.com/istanbuljs/istanbul-reports/blob/590e6b0089f67b723a1fdf57bc7ccc080ff189d7/lib/html/index.js#L135-L137 66 | html: { 67 | // outputs the report in ./coverage/html 68 | subdir: 'html' 69 | } 70 | } 71 | }, 72 | // web server port 73 | port: 9876, 74 | // enable / disable colors in the output (reporters and logs) 75 | colors: true, 76 | // level of logging 77 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 78 | logLevel: config.LOG_INFO, 79 | // enable / disable watching file and executing tests whenever any file changes 80 | autoWatch: true, 81 | // start these browsers 82 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 83 | browsers: ['PhantomJS', 'Chrome'], 84 | // Continuous Integration mode 85 | // if true, Karma captures browsers, runs the tests and exits 86 | singleRun: false, 87 | // Concurrency level 88 | // how many browser should be started simultaneous 89 | concurrency: Infinity, 90 | // 设定终端上不输出 webpack 的打包信息 91 | webpackMiddleware: { 92 | noInfo: true 93 | }, 94 | // 用来预编译源代码的 webpack 配置,基本就是项目的 webpack 配置,但要去掉 entry 属性 95 | webpack: webpackTestConf, 96 | 97 | babelPreprocessor: { 98 | options: { 99 | presets: ['airbnb'] 100 | } 101 | }, 102 | }); 103 | }; 104 | 105 | 106 | // const path = require('path'); 107 | 108 | // let webpackTestConf = require('../../tools/webpack.base.js'); 109 | 110 | // // 给webpack基础配置增加一些针对测试环境的配置 111 | // webpackTestConf = Object.assign(webpackTestConf, { 112 | // // karma需要开启inline source map 113 | // devtool: "#inline-source-map", 114 | // // 针对enzyme的issue https://github.com/airbnb/enzyme/issues/503 115 | // externals: { 116 | // 'cheerio': 'window', 117 | // 'react/addons': true, 118 | // 'react/lib/ExecutionEnvironment': true, 119 | // 'react/lib/ReactContext': true 120 | // } 121 | // }) 122 | // module.exports = function (config) { 123 | // config.set({ 124 | 125 | // // 将被用于所有配置的基础路径 (eg. files, exclude) 126 | // basePath: '', 127 | 128 | // // 使用的测试框架 可用的框架: https://npmjs.org/browse/keyword/karma-adapter 129 | // frameworks: ['mocha'], 130 | 131 | // // Karma的入口文件 132 | // files: [ 133 | // //{pattern: path.join(__basename, 'node_modules/chai/chai.js'),include: true}, 134 | // './index.js' 135 | // ], 136 | 137 | // // 需排除的文件 138 | // exclude: [], 139 | 140 | // // 需要预处理的文件,比如需要webpack进行处理后再让karma运行服务器 可用的预处理器: 141 | // // https://npmjs.org/browse/keyword/karma-preprocessor 142 | // preprocessors: { 143 | // ['index.js']: ['webpack', 'sourcemap'] 144 | // }, 145 | 146 | // // 配置webpack 147 | // webpack: webpackTestConf, 148 | // webpackMiddleware: { 149 | // noInfo: true 150 | // }, 151 | 152 | // // 测试结果报告,覆盖率报告如有需要在这里配置 可用的报告插件: 153 | // // https://npmjs.org/browse/keyword/karma-reporter 154 | // reporters: [ 155 | // 'mocha', 'coverage' 156 | // ], 157 | 158 | // // mocha报告插件配置 159 | // mochaReporter: { 160 | // showDiff: true 161 | // }, 162 | 163 | // // 覆盖率报告插件配置 164 | // coverageReporter: { 165 | // dir: path.join(__dirname, 'coverage'), 166 | // reporters: [ 167 | // { 168 | // type: 'json', 169 | // subdir: '.', 170 | // file: 'coverage.json' 171 | // }, { 172 | // type: 'lcov', 173 | // subdir: '.' 174 | // }, { 175 | // type: 'text-summary' 176 | // } 177 | // ] 178 | // }, 179 | // // 服务器端口 180 | // port: 9876, 181 | 182 | // // 是否要有颜色 183 | // colors: true, 184 | 185 | // // logging的级别 可用值: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || 186 | // // config.LOG_INFO || config.LOG_DEBUG 187 | // logLevel: config.LOG_INFO, 188 | 189 | // // 是否监听文件变动 190 | // autoWatch: true, 191 | 192 | // // 启动下列这些浏览器 可用的启动器: https://npmjs.org/browse/keyword/karma-launcher 193 | // browsers: [ 194 | // 'Chrome', 'PhantomJS' 195 | // ], 196 | 197 | // // 持续集成模式 如果是true,Karma只会运行一次并退出 198 | // singleRun: true 199 | // }) 200 | // } 201 | -------------------------------------------------------------------------------- /src/spin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2011-2014 Felix Gnass 3 | * Licensed under the MIT license 4 | * http://spin.js.org/ 5 | * 6 | * Example: 7 | var opts = { 8 | lines: 12 // The number of lines to draw 9 | , length: 7 // The length of each line 10 | , width: 5 // The line thickness 11 | , radius: 10 // The radius of the inner circle 12 | , scale: 1.0 // Scales overall size of the spinner 13 | , corners: 1 // Roundness (0..1) 14 | , color: '#000' // #rgb or #rrggbb 15 | , opacity: 1/4 // Opacity of the lines 16 | , rotate: 0 // Rotation offset 17 | , direction: 1 // 1: clockwise, -1: counterclockwise 18 | , speed: 1 // Rounds per second 19 | , trail: 100 // Afterglow percentage 20 | , fps: 20 // Frames per second when using setTimeout() 21 | , zIndex: 2e9 // Use a high z-index by default 22 | , className: 'spinner' // CSS class to assign to the element 23 | , top: '50%' // center vertically 24 | , left: '50%' // center horizontally 25 | , shadow: false // Whether to render a shadow 26 | , hwaccel: false // Whether to use hardware acceleration (might be buggy) 27 | , position: 'absolute' // Element positioning 28 | } 29 | var target = document.getElementById('foo') 30 | var spinner = new Spinner(opts).spin(target) 31 | */ 32 | let prefixes = ['webkit', 'Moz', 'ms', 'O'], 33 | /* Vendor prefixes */ 34 | animations = {}, 35 | /* Animation rules keyed by their name */ 36 | useCssAnimations, /* Whether to use CSS animations or setTimeout */ 37 | sheet; /* A stylesheet to hold the @keyframe or VML rules. */ 38 | 39 | /** 40 | * Utility function to create elements. If no tag name is given, 41 | * a DIV is created. Optionally properties can be passed. 42 | */ 43 | function createEl(tag, prop) { 44 | let el = document.createElement(tag || 'div'), 45 | n; 46 | 47 | for (n in prop) el[n] = prop[n]; 48 | return el; 49 | } 50 | 51 | /** 52 | * Appends children and returns the parent. 53 | */ 54 | function ins(parent /* child1, child2, ... */) { 55 | for (let i = 1, n = arguments.length; i < n; i++) { 56 | parent.appendChild(arguments[i]); 57 | } 58 | 59 | return parent; 60 | } 61 | 62 | /** 63 | * Creates an opacity keyframe animation rule and returns its name. 64 | * Since most mobile Webkits have timing issues with animation-delay, 65 | * we create separate rules for each line/segment. 66 | */ 67 | function addAnimation(alpha, trail, i, lines) { 68 | let name = ['opacity', trail, ~~(alpha * 100), i, lines].join('-'), 69 | start = 0.01 + i / lines * 100, 70 | z = Math.max(1 - (1 - alpha) / trail * (100 - start), alpha), 71 | prefix = useCssAnimations.substring(0, useCssAnimations.indexOf('Animation')).toLowerCase(), 72 | pre = prefix && '-' + prefix + '-' || ''; 73 | 74 | if (!animations[name]) { 75 | sheet.insertRule( 76 | '@' + pre + 'keyframes ' + name + '{' + 77 | '0%{opacity:' + z + '}' + 78 | start + '%{opacity:' + alpha + '}' + 79 | (start + 0.01) + '%{opacity:1}' + 80 | (start + trail) % 100 + '%{opacity:' + alpha + '}' + 81 | '100%{opacity:' + z + '}' + 82 | '}', sheet.cssRules.length); 83 | 84 | animations[name] = 1; 85 | } 86 | 87 | return name; 88 | } 89 | 90 | /** 91 | * Tries various vendor prefixes and returns the first supported property. 92 | */ 93 | function vendor(el, prop) { 94 | let s = el.style, 95 | pp, 96 | i; 97 | 98 | prop = prop.charAt(0).toUpperCase() + prop.slice(1); 99 | if (typeof s[prop] !== 'undefined') return prop; 100 | for (i = 0; i < prefixes.length; i++) { 101 | pp = prefixes[i] + prop; 102 | if (typeof s[pp] !== 'undefined') return pp; 103 | } 104 | } 105 | 106 | /** 107 | * Sets multiple style properties at once. 108 | */ 109 | function css(el, prop) { 110 | for (let n in prop) { 111 | el.style[vendor(el, n) || n] = prop[n]; 112 | } 113 | 114 | return el; 115 | } 116 | 117 | /** 118 | * Fills in default values. 119 | */ 120 | function merge(obj) { 121 | for (let i = 1; i < arguments.length; i++) { 122 | let def = arguments[i]; 123 | 124 | for (let n in def) { 125 | if (typeof obj[n] === 'undefined') obj[n] = def[n]; 126 | } 127 | } 128 | return obj; 129 | } 130 | 131 | /** 132 | * Returns the line color from the given string or array. 133 | */ 134 | function getColor(color, idx) { 135 | return typeof color === 'string' ? color : color[idx % color.length]; 136 | } 137 | 138 | // Built-in defaults 139 | 140 | let defaults = { 141 | lines: 12, // The number of lines to draw 142 | length: 7, // The length of each line 143 | width: 5, // The line thickness 144 | radius: 10, // The radius of the inner circle 145 | scale: 1.0, // Scales overall size of the spinner 146 | corners: 1, // Roundness (0..1) 147 | color: '#000', // #rgb or #rrggbb 148 | opacity: 1 / 4, // Opacity of the lines 149 | rotate: 0, // Rotation offset 150 | direction: 1, // 1: clockwise, -1: counterclockwise 151 | speed: 1, // Rounds per second 152 | trail: 100, // Afterglow percentage 153 | fps: 20, // Frames per second when using setTimeout() 154 | zIndex: 2e9, // Use a high z-index by default 155 | className: 'spinner', // CSS class to assign to the element 156 | top: '50%', // center vertically 157 | left: '50%', // center horizontally 158 | shadow: false, // Whether to render a shadow 159 | hwaccel: false, // Whether to use hardware acceleration (might be buggy) 160 | position: 'absolute' // Element positioning 161 | }; 162 | 163 | /** The constructor */ 164 | function Spinner(o) { 165 | this.opts = merge(o || {}, Spinner.defaults, defaults); 166 | } 167 | 168 | // Global defaults that override the built-ins: 169 | Spinner.defaults = {}; 170 | 171 | merge(Spinner.prototype, { 172 | /** 173 | * Adds the spinner to the given target element. If this instance is already 174 | * spinning, it is automatically removed from its previous target b calling 175 | * stop() internally. 176 | */ 177 | spin: function(target) { 178 | this.stop(); 179 | 180 | let self = this, 181 | o = self.opts, 182 | el = self.el = createEl(null, { 183 | className: o.className 184 | }); 185 | 186 | css(el, { 187 | position: o.position, 188 | width: 0, 189 | zIndex: o.zIndex, 190 | left: o.left, 191 | top: o.top 192 | }); 193 | 194 | if (target) { 195 | target.insertBefore(el, target.firstChild || null); 196 | } 197 | 198 | el.setAttribute('role', 'progressbar'); 199 | self.lines(el, self.opts); 200 | 201 | if (!useCssAnimations) { 202 | // No CSS animation support, use setTimeout() instead 203 | let i = 0, 204 | start = (o.lines - 1) * (1 - o.direction) / 2, 205 | alpha, 206 | fps = o.fps, 207 | f = fps / o.speed, 208 | ostep = (1 - o.opacity) / (f * o.trail / 100), 209 | astep = f / o.lines; 210 | (function anim() { 211 | i++; 212 | for (let j = 0; j < o.lines; j++) { 213 | alpha = Math.max(1 - (i + (o.lines - j) * astep) % f * ostep, o.opacity); 214 | 215 | self.opacity(el, j * o.direction + start, alpha, o); 216 | } 217 | self.timeout = self.el && setTimeout(anim, ~~(1000 / fps)); 218 | })(); 219 | } 220 | return self; 221 | }, 222 | 223 | /** 224 | * Stops and removes the Spinner. 225 | */ 226 | stop: function() { 227 | let el = this.el; 228 | 229 | if (el) { 230 | clearTimeout(this.timeout); 231 | if (el.parentNode) el.parentNode.removeChild(el); 232 | this.el = null; 233 | } 234 | return this; 235 | }, 236 | 237 | /** 238 | * Internal method that draws the individual lines. Will be overwritten 239 | * in VML fallback mode below. 240 | */ 241 | lines: function(el, o) { 242 | let i = 0, 243 | start = (o.lines - 1) * (1 - o.direction) / 2, 244 | seg; 245 | 246 | function fill(color, shadow) { 247 | return css(createEl(), { 248 | position: 'absolute', 249 | width: o.scale * (o.length + o.width) + 'px', 250 | height: o.scale * o.width + 'px', 251 | background: color, 252 | boxShadow: shadow, 253 | transformOrigin: 'left', 254 | transform: 'rotate(' + ~~(360 / o.lines * i + o.rotate) + 'deg) translate(' + 255 | o.scale * o.radius + 'px' + ',0)', 256 | borderRadius: (o.corners * o.scale * o.width >> 1) + 'px' 257 | }); 258 | } 259 | 260 | for (; i < o.lines; i++) { 261 | seg = css(createEl(), { 262 | position: 'absolute', 263 | top: 1 + ~(o.scale * o.width / 2) + 'px', 264 | transform: o.hwaccel ? 'translate3d(0,0,0)' : '', 265 | opacity: o.opacity, 266 | animation: useCssAnimations && addAnimation(o.opacity, o.trail, start + i * 267 | o.direction, o.lines) + ' ' + 1 / o.speed + 's linear infinite' 268 | }); 269 | 270 | if (o.shadow) {ins(seg, css(fill('#000', '0 0 4px #000'), { 271 | top: '2px' 272 | }));} 273 | ins(el, ins(seg, fill(getColor(o.color, i), '0 0 1px rgba(0,0,0,.1)'))); 274 | } 275 | return el; 276 | }, 277 | 278 | /** 279 | * Internal method that adjusts the opacity of a single line. 280 | * Will be overwritten in VML fallback mode below. 281 | */ 282 | opacity: function(el, i, val) { 283 | if (i < el.childNodes.length) el.childNodes[i].style.opacity = val; 284 | } 285 | 286 | }); 287 | 288 | function initVML() { 289 | /* Utility function to create a VML tag */ 290 | function vml(tag, attr) { 291 | return createEl('<' + tag + ' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">', attr); 292 | } 293 | 294 | // No CSS transforms but VML support, add a CSS rule for VML elements: 295 | sheet.addRule('.spin-vml', 'behavior:url(#default#VML)'); 296 | 297 | Spinner.prototype.lines = function(el, o) { 298 | let r = o.scale * (o.length + o.width), 299 | s = o.scale * 2 * r; 300 | 301 | function grp() { 302 | return css( 303 | vml('group', { 304 | coordsize: s + ' ' + s, 305 | coordorigin: -r + ' ' + -r 306 | }), { 307 | width: s, 308 | height: s 309 | } 310 | ); 311 | } 312 | 313 | let margin = -(o.width + o.length) * o.scale * 2 + 'px', 314 | g = css(grp(), { 315 | position: 'absolute', 316 | top: margin, 317 | left: margin 318 | }), 319 | i; 320 | 321 | function seg(i, dx, filter) { 322 | ins( 323 | g, ins( 324 | css(grp(), { 325 | rotation: 360 / o.lines * i + 'deg', 326 | left: ~~dx 327 | }), ins( 328 | css( 329 | vml('roundrect', { 330 | arcsize: o.corners 331 | }), { 332 | width: r, 333 | height: o.scale * o.width, 334 | left: o.scale * o.radius, 335 | top: -o.scale * o.width >> 1, 336 | filter: filter 337 | } 338 | ), vml('fill', { 339 | color: getColor(o.color, i), 340 | opacity: o.opacity 341 | }), vml('stroke', { 342 | opacity: 0 343 | }) // transparent stroke to fix color bleeding upon opacity change 344 | ) 345 | ) 346 | ); 347 | } 348 | 349 | if (o.shadow) { 350 | for (i = 1; i <= o.lines; i++) { 351 | seg(i, -2, 'progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)'); 352 | } 353 | } 354 | 355 | for (i = 1; i <= o.lines; i++) seg(i); 356 | return ins(el, g); 357 | }; 358 | 359 | Spinner.prototype.opacity = function(el, i, val, o) { 360 | let c = el.firstChild; 361 | 362 | o = o.shadow && o.lines || 0; 363 | if (c && i + o < c.childNodes.length) { 364 | c = c.childNodes[i + o]; 365 | c = c && c.firstChild; 366 | c = c && c.firstChild; 367 | if (c) c.opacity = val; 368 | } 369 | }; 370 | } 371 | 372 | if (typeof document !== 'undefined') { 373 | sheet = (function() { 374 | let el = createEl('style', { 375 | type: 'text/css' 376 | }); 377 | 378 | ins(document.getElementsByTagName('head')[0], el); 379 | return el.sheet || el.styleSheet; 380 | })(); 381 | 382 | let probe = css(createEl('group'), { 383 | behavior: 'url(#default#VML)' 384 | }); 385 | 386 | if (!vendor(probe, 'transform') && probe.adj) initVML(); 387 | else useCssAnimations = vendor(probe, 'animation'); 388 | } 389 | 390 | export default Spinner; 391 | --------------------------------------------------------------------------------