shadow-cljs: JavaScript 依赖的实践

原文 https://code.thheller.com/blo...
原做者是 shadow-cljs 做者, shadow-cljs 是一个面向 JavaScript 开发者友好的 ClojureScript 编译器.html

以前关于 js 依赖的文章(问题, 前景)里面, 我解释了为何 shadow-cljs 当中采用了和 ClojureScript 默认的方案不一样的作法. 简单回顾下:node

  • cljsjs 或者 :foreign-libs 的写法难以扩张react

  • 自定义的打包实际当中使用繁琐git

  • Closure Compiler 目前对大部分的 npm 模块的处理不够可靠github

  • shadow-cljs 自定义了一个 js bundler, 而移除了 :foreign-libs 的支持npm

安装 js 依赖

几乎全部的 npm 模块都会写一遍如何安装. 如今对于 shadow-cljs 来讲也是适用的. 好比有个类库要你运行:json

npm install the-thing

你照作就好. 不须要其余步骤了. 固然你喜欢的话能够用 yarn. 而后依赖就会被写进 package.json 文件用于管理. 若是没有 package.json 那就运行 npm init.浏览器

上面说到这些东西, 你能够用这个 QuickStart 模板 来试用.bash

试用 js 依赖

大部分的 npm 模块也会写一下具体的代码表示怎样使用模块. "旧的" CommonJS 的写法是用 require 调用. 翻译到 ClojureScript 就是:app

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.

相关文章
相关标签/搜索