shadow-cljs: JavaScript 依赖的实践

1

原文 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 当中做的不一样, 以前比如引入了 thingns 然后要用 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]). 也就是说会查找 <project>/node_modules/thing/... 当中的代码. 为了对这个行为进行自定义, 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.


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

clojure · 2018年05月06日

谢谢作者

回复

载入中...