原文 https://code.thheller.com/blo...
原作者是 shadow-cljs 作者, shadow-cljs 是一个面向 JavaScript 开发者友好的 ClojureScript 编译器.
之前关于 js 依赖的文章(问题, 前景)里面, 我解释了为什么 shadow-cljs
当中采用了和 ClojureScript 默认的方案不同的做法. 简单回顾下:
cljsjs
或者:foreign-libs
的写法难以扩张自定义的打包实际当中使用繁琐
Closure Compiler 目前对大部分的 npm 模块的处理不够可靠
shadow-cljs
自定义了一个 js bundler, 而移除了:foreign-libs
的支持
安装 js 依赖
几乎所有的 npm 模块都会写一遍如何安装. 现在对于 shadow-cljs
来说也是适用的. 比如有个类库要你运行:
npm install the-thing
你照做就好. 不需要其他步骤了. 当然你喜欢的话可以用 yarn
. 然后依赖就会被写进 package.json
文件用于管理. 如果没有 package.json
那就运行 npm init
.
上面说到这些东西, 你可以用这个 QuickStart 模板 来试用.
试用 js 依赖
大部分的 npm 模块也会写一下具体的代码表示怎样使用模块. "旧的" CommonJS 的写法是用 require
调用. 翻译到 ClojureScript 就是:
var react = require('react');
(ns my.app
(:require ["react" :as react]))
不管 "string"
参数的地方用了什么然后被 require
调用, 我们都是这样换成 ns
:require
. :as
的 alias 部分就随你定义. 有了这个之后, 它就像是其他的 cljs 的命名空间那样可以调用了:
(react/createElement "div" nil "helle world")
这跟以前 :foreign-libs
或者 CLJSJS
当中做的不一样, 以前比如引入了 thing
到 ns
然后要用 js/Thing
(或者其他全局导出的变量)来使用代码. 现在可以用 ns
格式以及 :as
后面提供的名称. 需要的话还可以写 :refer
和 :rename
.
一些模块会暴露一个函数, 那你可以写 (:require ["thing" as thing])
然后调用 (thing)
.
最近一些模块开始用 ES6 的 import
语法作为例子了. 这些代码除了一个 default
写法以外, 基本上在 ClojureScript 能做到一一对应. 比如说翻译下面的例子:
import defaultExport from "module-name";
import * as name from "module-name";
import { export } from "module-name";
import { export as alias } from "module-name";
import { export1 , export2 } from "module-name";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
到(包裹在 ns
里面的):
(:require ["module-name" :default defaultExport])
(:require ["module-name" :as name])
(:require ["module-name" :refer (export)])
(:require ["module-name" :rename {export alias}])
(:require ["module-name" :refer (export1) :rename {export2 alias2}])
(:require ["module-name" :refer (export) :default defaultExport])
(:require ["module-name" :as name :default defaultExport])
(:require ["module-name"])
其中 :default
参数目前只在 shadow-cljs
里面支持, 但是你也可以在这里投票帮助它进入到规范当中. 或者你也可以一直用 :as alias
然后调用 alias/default
, 这样你觉得能个标准的 cljs 始终保持兼容的话. 我觉得吧, 对于某些模块来说啰嗦了点.
新的可能性
之前我们的使用打包之后的代码, 可能会包含我们用不到的代码. 某些模块也说明了一些办法可以只引入部分的模块, 这样最终构建的代码体积会小一些.
react-virtualized 有个这样的例子:
// You can import any component you want as a named export from 'react-virtualized', eg
import { Column, Table } from 'react-virtualized'
// But if you only use a few react-virtualized components,
// And you're concerned about increasing your application's bundle size,
// You can directly import only the components you need, like so:
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer'
import List from 'react-virtualized/dist/commonjs/List'
那么很容易翻译过去:
;; all
(:require ["react-virtualized" :refer (Column Table)])
;; one by one
(:require ["react-virtualized/dist/commonjs/AutoSizer" :default virtual-auto-sizer])
(:require ["react-virtualized/dist/commonjs/List" :default virtual-list])
查找 js 依赖
默认情况下 shadow-cljs
通过 npm 的方式引用查找所有 (:require ["thing" :as x])
. 也就是说会查找
当中的代码. 为了对这个行为进行自定义, shadow-cljs
暴露了一个 :resolve
配置项, 你可以自己定义某些模块如何查找.
使用 CDN
比如说页面里的 React
从 CDN 上引用了. 这时候按说你可以用 js/React
了, 但是最好还是不要这样. 你应该是继续用 (:require ["react" :as react])
, 同时在 shadow-cljs
里定义 react
怎样查找. 这个配置在 shadow-cljs.edn
文件里配置:
{:builds
{:app
{:target :browser
...
:js-options
{:resolve {"react" {:target :global
:global "React"}}}}
:server
{:target :node-script
...}}}
现在 :app
这个构建会使用全局的 React
实例, 而在 :server
这个构建当中会继续使用 react
的 npm 模块. 不需要额外折腾代码去完成需求.
重定向 require
某些模块提供多个 "dist" 文件, 然后可能默认的那个刚好是在 shadow-cljs
里有问题的. 一个明显的例子就是 d3
. 他们默认的 "main"
指向 build/d3.node.js
, 这个不是在浏览器里面用的版本. 他们的 ES6 代码还触发了 Closure Compiler 里的一个 bug, 所以我们不能用. 这样的话我们就重定向到其他的引用去:
{:resolve {"d3" {:target :npm
:require "d3/build/d3.js"}}}
你也可以直接就写 (:require ["d3/build/d3.js" :as d3])
, 如果你只关心浏览器当中的使用的话.
使用本地文件
你还可以用 :resolve
来直接映射到一个项目中的本地文件:
{:resolve {"my-thing" {:target :file
:file "path/to/file.js"}}}
这里的 :file
总是相对于项目根路径. 这个文件里可以用 require
或者 import/export
, 这些随后都会被处理好的.
迁移 cljsjs.*
很多 cljs 类库还在用 CLJSJS
包, 它们在 shadow-cljs
里不能正常使用了, 因为 :foreign-libs
不再支持. 我提供了一个清晰的迁移路线, 只需要增加一个 shim 文件把 cljsjs.thing
映射回到原始的 npm
模块, 然后把全局变量暴露出去.
比如 react
需要一个这样的文件src/cljsjs/react.cljs:
(ns cljsjs.react
(:require ["react" :as react]
["create-react-class" :as crc]))
(js/goog.object.set react "createClass" crc)
(js/goog.exportSymbol "React" react)
因为这样的话每个人手动处理会麻烦, 所以我提供了 shadow-cljsjs
这个类库来提供这个功能. 虽然不会包含每一个模块, 但是我会持续添加. 欢迎来帮忙贡献模块.
不过它仅仅提供 shim 文件. 你还是需要用 npm install
安装真实的模块.
其他功能不能用怎么办?
JavaScript 社区变化很快, 并不是每个人都一样地写代码, 都一样地分发代码, 有些东西是 shadow-cljs
不能自动处理或者需要自定义 :resolve
配置的. 可能是会遇到 bug, 毕竟都是新东西.
遇到任何模块不能按照预期地使用, 请报告. 在 #shadow-cljs 很容易找到我.
关于这篇文章的讨论请移步 :clojurevese.