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.

相關文章
相關標籤/搜索