diff --git a/package.json b/package.json index ce6260f..82da577 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "react-async-script-loader", - "version": "0.1.1", + "version": "0.2.3", "description": "A decorator for script lazy loading on react component", "main": "lib/index.js", "scripts": { - "build": "babel module --out-dir lib", + "build": "babel src --out-dir lib", "prepublish": "npm run build", "test": "karma start" }, @@ -24,12 +24,11 @@ }, "homepage": "https://github.com/leozdgao/react-script-loader#readme", "peerDependencies": { - "react": "^0.14.0" + "react": "^15.0.1 || ^16.0.0" }, "devDependencies": { "babel-cli": "^6.1.18", "babel-eslint": "^4.1.5", - "babel-jest": "^9.0.3", "babel-loader": "~6.2.4", "babel-plugin-syntax-class-properties": "^6.1.18", "babel-plugin-transform-class-properties": "^6.1.20", @@ -38,7 +37,6 @@ "babel-preset-stage-0": "^6.1.18", "chai": "~3.5.0", "eslint": "^1.9.0", - "jest-cli": "^0.9.2", "karma": "~0.13.22", "karma-chai": "~0.1.0", "karma-mocha": "~0.2.2", @@ -48,20 +46,13 @@ "karma-webpack": "~1.7.0", "mocha": "~2.4.5", "phantomjs-prebuilt": "~2.1.7", - "react": "^0.14.7", - "react-addons-test-utils": "^0.14.7", - "react-dom": "^0.14.7", - "sinon": "~1.17.3", + "prop-types": "~15.5.8", + "react": "^15.0.1", + "react-addons-test-utils": "^15.0.1", + "react-dom": "^15.0.1", "webpack": "~1.12.14" }, "dependencies": { "hoist-non-react-statics": "^1.0.3" - }, - "jest": { - "unmockedModulePathPatterns": [ - "/node_modules/react", - "/node_modules/react-dom", - "/node_modules/react-addons-test-utils" - ] } } diff --git a/src/index.js b/src/index.js index 77ed39a..93b1492 100644 --- a/src/index.js +++ b/src/index.js @@ -1,39 +1,81 @@ -import React, { Component, PropTypes as T } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import hoistStatics from 'hoist-non-react-statics' -import { isDefined, newScript, series, noop } from './utils' +import { newScript, series, noop } from './utils' const loadedScript = [] +const pendingScripts = {} let failedScript = [] -const scriptLoader = (...scripts) => (WrappedComponent) => { +export function startLoadingScripts(scripts, onComplete = noop) { + // sequence load + const loadNewScript = (script) => { + const src = typeof script === 'object' ? script.src : script + if (loadedScript.indexOf(src) < 0) { + return taskComplete => { + const callbacks = pendingScripts[src] || [] + callbacks.push(taskComplete) + pendingScripts[src] = callbacks + if (callbacks.length === 1) { + return newScript(script)(err => { + pendingScripts[src].forEach(cb => cb(err, src)) + delete pendingScripts[src] + }) + } + } + } + } + const tasks = scripts.map(src => { + if (Array.isArray(src)) { + return src.map(loadNewScript) + } + else return loadNewScript(src) + }) - const addCache = (entry) => { - if (loadedScript.indexOf(entry) < 0) { - loadedScript.push(entry) + series(...tasks)((err, src) => { + if (err) { + failedScript.push(src) + } + else { + if (Array.isArray(src)) { + src.forEach(addCache) + } + else addCache(src) } + })(err => { + removeFailedScript() + onComplete(err) + }) +} + +const addCache = (entry) => { + if (loadedScript.indexOf(entry) < 0) { + loadedScript.push(entry) } +} - const removeFailedScript = () => { - if (failedScript.length > 0) { - failedScript.forEach((script) => { - const node = document.querySelector(`script[src='${script}']`) - if (node != null) { - node.parentNode.removeChild(node) - } - }) +const removeFailedScript = () => { + if (failedScript.length > 0) { + failedScript.forEach((script) => { + const node = document.querySelector(`script[src='${script}']`) + if (node != null) { + node.parentNode.removeChild(node) + } + }) - failedScript = [] - } + failedScript = [] } +} +const scriptLoader = (...scripts) => (WrappedComponent) => { class ScriptLoader extends Component { static propTypes = { - onScriptLoaded: T.func - }; + onScriptLoaded: PropTypes.func + } static defaultProps = { onScriptLoaded: noop - }; + } constructor (props, context) { super(props, context) @@ -42,48 +84,39 @@ const scriptLoader = (...scripts) => (WrappedComponent) => { isScriptLoaded: false, isScriptLoadSucceed: false } + + this._isMounted = false; } componentDidMount () { - // sequence load - const loadNewScript = (src) => { - if (loadedScript.indexOf(src) < 0) return newScript(src) - } - const tasks = scripts.map(src => { - if (Array.isArray(src)) { - return src.map(loadNewScript) + this._isMounted = true; + startLoadingScripts(scripts, err => { + if(this._isMounted) { + this.setState({ + isScriptLoaded: true, + isScriptLoadSucceed: !err + }, () => { + if (!err) { + this.props.onScriptLoaded() + } + }) } - else return loadNewScript(src) }) + } - series(...tasks)((err, src) => { - if (err) { - failedScript.push(src) - } - else { - if (Array.isArray(src)) { - src.forEach(addCache) - } - else addCache(src) - } - })(err => { - removeFailedScript() - - this.setState({ - isScriptLoaded: true, - isScriptLoadSucceed: !err - }, () => { - if (!err) { - this.props.onScriptLoaded() - } - }) - }) + componentWillUnmount () { + this._isMounted = false; + } + + getWrappedInstance () { + return this.refs.wrappedInstance; } render () { const props = { ...this.props, - ...this.state + ...this.state, + ref: 'wrappedInstance' } return ( diff --git a/src/utils.js b/src/utils.js index d92b9e1..6ad07be 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,12 +3,22 @@ export const isFunction = val => typeof val === 'function' export const noop = _ => { } export const newScript = (src) => (cb) => { - const script = document.createElement('script') - script.src = src - script.addEventListener('load', () => cb(null, src)) - script.addEventListener('error', () => cb(true, src)) - document.body.appendChild(script) - return script + const scriptElem = document.createElement('script') + if (typeof src === 'object') { + // copy every property to the element + for (var key in src) { + if (Object.prototype.hasOwnProperty.call(src, key)) { + scriptElem[key] = src[key]; + } + } + src = src.src; + } else { + scriptElem.src = src + } + scriptElem.addEventListener('load', () => cb(null, src)) + scriptElem.addEventListener('error', () => cb(true, src)) + document.body.appendChild(scriptElem) + return scriptElem } const keyIterator = (cols) => { diff --git a/test/index.spec.js b/test/index.spec.js index b02b195..318f242 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,4 +1,4 @@ -const ReactTestUtils = require('react-addons-test-utils') +const ReactTestUtils = require('react-dom/test-utils') const React = require('react') const AsyncScriptLoader = require('../src').default @@ -18,7 +18,6 @@ function renderTestComponent (deps, onScriptLoaded) { function checkScriptLoaded (getComponent, done) { return _ => { const com = getComponent() - console.log(com) expect(com.props.isScriptLoaded).to.be.true expect(com.props.isScriptLoadSucceed).to.be.true @@ -30,9 +29,11 @@ function checkScriptLoaded (getComponent, done) { describe('Test this module', _ => { it('[react-async-script-loader] Load external script after component mounted', function (done) { - const deps = [ '//cdn.bootcss.com/jquery/2.2.1/jquery.min.js' ] + const deps = [ 'https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js' ] const com = renderTestComponent(deps, onScriptLoaded) + this.timeout(5000) + // check script tags deps.forEach(testScript => { const tag = document.querySelector(`script[src='${testScript}']`) @@ -54,11 +55,13 @@ describe('Test this module', _ => { it('[react-async-script-loader] No redundant script tag will be appended', function (done) { - const deps = [ '//cdn.bootcss.com/jquery/2.2.1/jquery.min.js' ] + const deps = [ '//cdn.bootcss.com/jquery/2.1.1/jquery.min.js' ] const com0 = renderTestComponent(deps, checkScriptLoaded(_ => com0, checkAllDone)) const com1 = renderTestComponent(deps, checkScriptLoaded(_ => com1, checkAllDone)) let count = 0 + this.timeout(5000) + // check script tags deps.forEach(testScript => { const tags = document.querySelectorAll(`script[src='${testScript}']`) diff --git a/test/utils.spec.js b/test/utils.spec.js index 4cc97c9..ef732c8 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -5,7 +5,7 @@ describe('Test util functions', _ => { const taskBundle = [1, 2, 3, 4, 5].map(testTask) it('[utils/newScript] A thunk task, append new script tag', function (done) { - const testScript = '//cdn.bootcss.com/jquery/2.2.1/jquery.min.js' + const testScript = 'https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js' const task = newScript(testScript) // start task @@ -20,6 +20,22 @@ describe('Test util functions', _ => { done() }) }) + it('[utils/newScript] A thunk task, append new script tag with id', function (done) { + const testScript = 'https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js' + const task = newScript({src: testScript, id: 'test'}) + + // start task + task((err, src) => { + const tag = document.querySelector(`script[src='${testScript}']`) + + // assert + expect(err).to.not.exist + expect(src).to.equal(testScript) + expect(tag).to.exist + + done() + }) + }) it('[utils/parallel] Run thunk task in parallel mode', function (done) { const startTime = Date.now()