- Notifications
You must be signed in to change notification settings - Fork3
cross-js/cross-js
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Adopting the CrossJS style means your JavaScript can work in any environment without being dependent on any core browser/node JS API and can work in any context, without unnecessarily increasing the bundle size. This might not make sense for all projects and development cultures. These rules certainly don't apply if you are only targeting one platform that has the API built in, but it makes sense for single modules.
By only includingreadable-stream in browser without anything else you have included buffer, events, string_decoder and inherits modules among many more smaller modules and already broke 4 of these rules and increased your bundle to:
| gzip | uncompressed | |
|---|---|---|
| unminified | 43.73KB | 174.77KB |
| minified | 19.79KB | 67.93KB |
Some Read Up:
Summary: Javascript is now the highest performance hit on many websites... One large image is able to load faster than JS. I also recommend that you try outlighthouse and perform a performance test.If you follow this rule you might be able to write code with just a fraction of what you otherwise would need.
- Don't use fs
- Don't use Buffer
- Don't use EventEmitter or EventTarget
- Don't create Node or Web readable Stream yourself
- Don't use any ajax/request library
- Don't use node's Url or querystring
- Don't use node's string_decoder
- Don't use inherits
- Don't use if-else platform specific code inside functions
- Don't depend of things that would make your application crash in another context
- Don't use extensionless import
- Don't use anything else then javascript
- Don't use cancelable promises
... or theFileReader
To understand the concept of writing cross platform application then you must understanding theonion architectureTry to develop your package with as little dependencies or knowledge of the platform you are running your code on, try to think as if your module was running on a sandboxed enviorment (or web worker) with no filesystem or network access, or no access to node or browser API's.The core layer of your application should be like a stdin and stdout. How the consumer reads and saves data should be entirely up to the the developers using your package.Deno requires modules to ask for permission to use fs/net. It feels safer to provide data to a third party package that does the transformation for you and gives you data back, rather than giving it read/write permissions to an entire folder.
Here is a senario: Say you have developed a tool that can encode/decode csv data to/from json. For it to work in node, deno and browser, it shouldn't be responsible for reading and saving files. The input data can come from many sources such as network, blob, fs, web socket and the output can have many destinations as well:
// A node developer would use it like thisasyncReadIterator=fs.createReadStream(file)asyncJsonIterator=csv_to_json(asyncReadIterator)forawait(letrowofasyncJsonIterator)http.request(row.url)// A browser developer would use it like thisasyncReadIterator=blob.stream()asyncJsonIterator=csv_to_json(asyncReadIterator)forawait(letrowofasyncJsonIterator)awaitfetch(row.url)
Only the end developer that is on the top of the onion structure and sitting in just one enviroment is allowed to use fs, but as soon as you allow someone else to use your package or it starts to be cross platform compatible, then it should be forbidden.
Uint8Array andBuffer have a lot in common and are very similar to each other.Addingbuffer will increase your bundle size a lot. And the fact that buffer inherits fromUint8Array have made recent Node.js core API's accept typed arrays. For example:fs.writeFile() used to only accept a Buffer but now works with both typed arrays and buffers.
Following this rule doesn't mean you have to convert all buffers you receive from node's core API and other modules from buffer to UInt8Array, just treat the buffer as a Uint8Array instead since buffer inherits from it.
UseUint8Array andDataView
// ✗ avoidconstchunk=Buffer.from(source)constchunk=Buffer.alloc(n)constchunk=Buffer.allocUnsafe(n)constchunk=newBuffer(source)// ✓ ok// Buffer allocationconstchunk=newUint8Array(n)// Buffer from An array-like or iterable object to convert to a typed array.constchunk=Uint8Array.from(source[,mapFn[,thisArg]])// Buffer from stringconstchunk=newTextEncoder().encode('abc')// Buffer from base64 (minimalist, there are synchronous way to, try avoiding base64 in the first place)constarrayBuffer=awaitfetch(`data:;base64,${string}`).then(r=>r.arrayBuffer())constchunk=newUint8Array(arrayBuffer)
Don't useEventEmitter
UPDATE: EventTarget have been added to NodeJS and that is now considered best, it hasonce, andsignal support also and works in deno, node and browsers,addEventListener(name, fn, {signal, once: true})
Don't take this seriously, sometimes it can be good to have more then one listener of one type registered. Also if you need something that can bubble up & down. IMHO I think that using events can increase the complexity of some application. It could certainly be avoided by other means without depending on other modules. In the end it will just increase the bundle size. Use them if it makes sense.
All I'm saying is:
Think twice before you decide to use them and if you really need it.
There is more than one way to skin a cat
- IncludingEventEmitter comes with a bundle cost.
- Also, how often do you need to subscribe to some event more than twice?
Often you know all the event you want to subscribe to beforehand, so why not just pass those down in the constructor or the function instead
Update NodeJS have introduced EventTarget and if you choose to use some kind of event handeling then I would suggest that you use EventTarget instead of EventEmitter, one new cool thing about EventTarget is that you can use AbortSignal (now also introduced to NodeJS core) as a way to also stop listening to multiple events as you callabortController.abort()
// ✗ avoidimportEventEmitterfrom'node:event'classFooextendsEventEmitter{}source.on(event,fn)source.once(event,fn)source.addEventListener(event)source.addEventListener(event,{once:true})source.dispatchEvent(event)source.emit(event)// Some suggestion:// ✓ ok (using setter/getter)classFoo{getonmessage(){...}setonmessage(x){...}}// ✓ ok (extending EventTarget)classBarextendsEventTarget{}// ✓ ok (passing in the events you want to subscribe to)classBar{constructor(opts){this.opts=opts}somethingHappens(){// Bail early, no need to continueif(!this.opts.progress)returnconsttotalDownload=havyCalculation(all_requests)this.opts.progress(totalDownload)}}constbar=newBar({onData(...args){...},onError(...args){...},onEnd(...args){...},})// stop listeningbar.opts.onData=null// Another example when reading a file / blob// How often do you need to listen to this events more then twice?constfr=newFileReader()fr.addEventListener('load',function(evt){...})fr.addEventListener('error',function(evt){...})fr.readAsArrayBuffer(blob)// I usually solve this by doing something like:constarrayBuffer=awaitnewResponse(blob).arrayBuffer()constjson=awaitnewResponse(blob).json()constiterable=newResponse(blob).bodyconsttext=awaitnewResponse(blob).text()// Worth mentioning that blob now has new methods blob.text(), blob.arrayBuffer() & blob.stream()// First two returns a promise
When a application knows what callback functions you have registered then there is no need for the application to compute and dispatch all events that nobody has subscribed to. The FileReader dispatches a progress and a loadend event that I'm not even subscribed to.
- Node streams are not available in browser and Web streams are not available in Node.
- Browserify node-stream will drag in the Buffer module as well and increase the size even more.
- Using them will create a lot of overhead stuff you don't even need.
Update: node v18 now have whatwg streams built in, but they are relatively slow, browser still lacks some functionallity.
Use iterator and/or asyncIterator.
Those are the minimum you will need to be able to create a producer and a consumer that can be both read and write.
// ✗ avoidimportstreamfrom'readable-stream'newReadableStream({...})newstream.Readable({...})// ✓ ok (create a async iterator that reads a large file/blob in chunks)asyncfunction*blobToIterator(blob,chunkSize=8<<16){// 0.5 MiBletposition=0while(true){constchunk=blob.slice(position,(position=position+chunkSize))if(!chunk.size)returnyieldnewUint8Array(awaitchunk.arrayBuffer())}}constiterable=blobToIterator(newBlob(['123']))// ✓ ok (convert a blob to a stream and read its iterator)conststream=blob.stream()constiterable=stream[Symbol.asyncIterator]()
There is no problem returning or consuming a stream you get from exampleResponse.body orfs.createReadStream() you can pass this stream around how much you like. Node streams have aSymbol.asyncIterator in the prototype and (web streams will eventually have them as well) you can add the symbol to your class also to make it a bit nicer
So you could just do this hack to transform a blob into a stream:
// ✓ okconstiterable=newResponse(blob).body// ✓ okconstiterable=blob.stream()// (chrome v76)// you don't create a stream yourself, you merely just transform a blob into an iterable stream ;)// a stream that has Symbol.asyncIterator
Then you can consume, pause, resume and pipe the iterator & streams (thanks due to Symbol.asyncIterator)All you need to do now is:
asyncfunction*transform(iterable){forawait(chunkofiterable){yielddo_transformation(chunk)}}// piping one iterator to another iteratoriterator=transform(get_iterable())
To make web streams iterable, you could use something like this:
import'fast-readable-async-iterator'// orif(typeofReadableStream!=='undefined'&&!ReadableStream.prototype[Symbol.asyncIterator]){ReadableStream.prototype[Symbol.asyncIterator]=function(){constreader=this.getReader()letlast=reader.read()return{next(){consttemp=lastlast=reader.read()returntemp},return(){returnreader.releaseLock()},throw(err){this.return()throwerr},[Symbol.asyncIterator](){returnthis}}}}
I think that your lower level api should be an (async)Iterator and provided to the user as is. If the user then wants to pipe it and do stuff with it then they could use eithernodes orWHATWG upcomming stream.from(iterable). It's also a good way to convert whatwg & node streams to one or the other (since both are @@asyncIterable) but noed stream should be avoided in a browser context and vise versa in the first place.
// should be left out of a lib and be used by the developer themself.ReadableStream.from(iterable||node_stream||whatwg_stream).pipeThrough(newTextEncoderStream())// convert text to uint8arrays.pipeThrough(newCompressionStream('gzip'))// compress bytes to gzip.pipeTo(destination).then(done,fail)// or in nodeimport{Readable}from'streamx'conststream=Readable.from(iterable||node_stream||whatwg_stream)
Update NodeJS v18 has fetch built in, use it instead.
UseFetch,Node-Fetch,isomorphic-fetch, orfetch-ponyfill
The idea here is to keep the bundle size small, and use less JIT compilation. Node servers only have to download and compilenode-fetch once.Deno also has fetch built right in, so no extra dependency is needed there, web workers don't have XMLHttpRequest, only fetch is supported so axios won't even work in web workers
// ✗ avoidimporthttpfrom'node:http'importhttpsfrom'node:https'importrequestfrom'request'importaxiosfrom'axios'importsuperagentfrom'superagent'constajax=jQuery.ajax// ✓ okimportfetch,{Headers,Response,Request}from'node-fetch'// browser excludeglobalThis.fetch// requires node v18
Something even better if you apply theonion architecture from the "Don't use the fs" rule section. Don't make the actual request. Instead construct a Request like object and pass it back to the developer so he/she can modify/make the request itself so i can use whatever http library they want. Think of it as not having any access to network request.
Don't use node'sUrl orquerystring
- TheWHATWG URL Standard uses a more selective and fine grained approach to selecting encoded characters than that used by the Legacy API.
- WHATWG URL and URLSearchParams is available in all contexts
- querystring will mix the value between string and arrays giving you an inconsistent api (see parse example below)
// ✗ avoidimportURLfrom'whatwg-url'importURLSearchParamsfrom'url-search-params'importquerystringfrom'node:querystring'importUrlfrom'node:url'constparsed=Url.parse(source)constobj=querystring.parse('a=a&abc=x&abc=y')// { a: 'a', abc: ['x', 'y'] }// ✓ okconstparsed=newURL(source)constparams=newURLSearchParams(source)
Don't use node'sstring_decoder
TextDecoder &TextEncoder can accomplish the same task but are available in both context
// ✗ avoidimportstringDecoderfrom'string_decoder'// ✓ okconst{ TextDecoder, TextEncoder}=globalThis
Don't useinherits
You can accomplish the same thing with class extends
// ✗ avoidimport{inherits}from'node:util'importinheritsfrom'inherits'// ✓ okclassFooextendsSomething{}
Doing the same check over and over again makes the compiler unable to garbage collect the unnecessary part
// ✗ avoidFoo.prototype.update=functionupdate(state){if(typeofrequestIdleCallback==='function'){requestIdleCallback(()=>{this.updateState(state)})}else{this.updateState(state)}}// ✓ okif(typeofrequestIdleCallback==='function'){Foo.prototype.update=functionupdate(state){requestIdleCallback(()=>{this.updateState(state)})}}else{Foo.prototype.update=Foo.prototype.updateState}
So it works everywhere.
Others are going to want to use your module but they may also use code testing/coverage inside NodeJS.
A good example of this is the ReadableStream asyncIterator polyfill shown above.
Or for some reason import your script to a web worker without using it.
Your script doesn't have to be fully functional, you can still write code that depends on the DOM or using the location to redirect to another page or use any node specific code likeprocess.nextTick. Even if you write a module that is targeted for only the browser.
If your file can be executed with this three method without crashing then you are golden.
node utils.js
<scripttype="module"src="utils.js"></script><script>newWorker('utils.js',{type:'module'})</script>
// utils.js// ✗ avoid// Use the same canvas and context for everythingconstcanvas=document.createElement('canvas')constctx=canvas.getContext('2d')// ✓ okletcanvasletctxif(typeofdocument==='object'){canvas=document.createElement('canvas')ctx=canvas.getContext('2d')}/** *@param {HTMLVideoElement} video the video element *@param {Function} cb function to call when done */functioncaptureFrame(video,cb){...}/** *@param {Image} the image to rotate *@param {Function} cb function to call when done */functionrotateFrame(img,cb){...}module.exports={ captureFrame, rotateFrame }
...or importing a index file with./
Browser can't just guess that it should fetch a resource that ends with.js orindex.jsit creates more burden on the compiler and it don't work with ESM
// ✗ avoidimportfoofrom'./foo'importindexfrom'./'// ✓ okimportfoofrom'./foo.js'importindexfrom'./index.js'
...such as PureScript, TypeScript, LiveScript or CoffeeScript that isn't able to run on any platform without beeing transpiled to javascript first
Read my other article on whyYou might not need TypeScript
The point is that:
- It should just work without any transpilation
- Transpilers just adds an additional building step, building time and the output is sometimes much larger
- Your project will be more complex.
- You are always going stay in the shadow of JavaScript and always have to wait for other transpilers to start supporting new syntax before you can use it.
- People should be able to include a part of your module without having to require the entire bundle or having to compile it themselves
- You need to educate developers to properly use other language other then what was built for the platform, while at the same time you need to know a bit of javascript to know what is going on
importxfrom'module/foo.js'
It should feel less like ajungleIts like trying to fit a square into a circle hole, and invading our platform. Have you looked at the output of a compiled file? It's much larger then if you had written it yourself.
you might not need typescript - by Eric Elliott
It's still fine to used.ts files to help with IDE.
You can also use jsDoc comment annotation, default params like so:
/** *@param {HTMLVideoElement} video the video element *@return {Promise<blob>} */functioncaptureFrame(video){...}
(Look github parse this so well with syntax color
using jsDoc you would be able to take full advantage of closure-compiler advanced optimizations alsoThe doc annotations works well with visual studio code also
Hold your horses and wait until Static Typing becomes a real thing:https://github.com/sirisian/ecmascript-typesIt's not fun to transpile your existing ts/flow back to js when it lands. So use jsDoc &d.ts for now.
There is a standard available and it's calledAbortController it was initially built for aborting a fetch request but they can be used for other things as well. There is a polyfill available and you can remove it once it becomes available later on (easier to refactor)
// ✗ avoidimport*from'p-cancelable'import*from'p-timeout'import*from'promise-cancelable'// ✓ okconstcontroller=newAbortController();constsignal=controller.signal;fetch(url,{ signal})// Latercontroller.abort();
Yes! but I have not made one myself, since so many love to usehttps://shields.io in their readme's you could include this to let people know that your code is using CrossJS style.
[](https://github.com/cross-js/cross-js)
About
Javascript guidelines for writing code in any context
Resources
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors3
Uh oh!
There was an error while loading.Please reload this page.