Latar Belakang
Suatu Sabtu malam saya sedang duduk dan mencari cara untuk membangun UI-Kit menggunakan webpack. Saya menggunakan styleguidst sebagai demo UI-kit. Tentu saja, webpack cerdas dan memasukkan semua file yang ada di direktori kerja ke dalam satu bundel dan dari sana semuanya berubah-ubah.
Saya membuat file entry.js, mengimpor semua komponen di sana, lalu mengekspor dari sana. Sepertinya semuanya baik-baik saja.
import Button from 'components/Button'
import Dropdown from 'components/Dropdown '
export {
Button,
Dropdown
}
Dan setelah mengumpulkan semua ini, saya mendapatkan output.js yang, seperti yang diharapkan, semuanya - semua komponen di heap dalam satu file. Di sini muncul pertanyaan:
Bagaimana cara mengumpulkan semua tombol, dropdown, dan sebagainya secara terpisah untuk diimpor ke proyek lain?Tetapi saya juga ingin mengunggahnya ke npm sebagai satu paket.
Hmm ... Ayo mulai berurutan.
Banyak masukan
Tentu saja, ide pertama yang muncul di benak Anda adalah mengurai semua komponen di direktori kerja. Saya harus google sedikit tentang parsing file, karena saya jarang bekerja dengan NodeJS. Menemukan hal seperti glob .
Kami mengemudi untuk menulis banyak entri.
const { basename, join, resolve } = require("path");
const glob = require("glob");
const componentFileRegEx = /\.(j|t)s(x)?$/;
const sassFileRegEx = /\s[ac]ss$/;
const getComponentsEntries = (pattern) => {
const entries = {};
glob.sync(pattern).forEach(file => {
const outFile = basename (file);
const entryName = outFile.replace(componentFileRegEx, "");
entries[entryName] = join(__dirname, file);
})
return entries;
}
module.exports = {
entry: getComponentsEntries("./components/**/*.tsx"),
output: {
filename: "[name].js",
path: resolve(__dirname, "build")
},
module: {
rules: [
{
test: componentFileRegEx,
loader: "babel-loader",
exclude: /node_modules/
},
{
test: sassFileRegEx,
use: ["style-loader", "css-loader", "sass-loader"]
}
]
}
resolve: {
extensions: [".js", ".ts", ".tsx", ".jsx"],
alias: {
components: resolve(__dirname, "components")
}
}
}
Selesai. Kami mengumpulkan.
Setelah perakitan, 2 file Button.js, Dropdown.js masuk ke direktori build - mari kita lihat ke dalam. Di dalam lisensi ada react.production.min.js, kode minified yang sulit dibaca, dan banyak omong kosong. Oke, coba gunakan tombolnya.
Di file demo tombol, ubah impor untuk diimpor dari direktori build.
Beginilah tampilan demo sederhana dari sebuah tombol di styleguidist - Button.md
```javascript
import Button from '../../build/Button'
<Button></Button>
```
Kami pergi untuk melihat tombol IR ... Pada tahap ini, ide dan keinginan untuk mengumpulkan melalui webpack sudah hilang.
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
Mencari jalur build lain tanpa webpack
Kami mencari bantuan untuk babel tanpa webpack. Kami menulis skrip di package.json, menentukan file konfigurasi, ekstensi, direktori tempat komponen berada, direktori tempat membangun:
{
//...package.json -
scripts: {
"build": "babel --config-file ./.babelrc --extensions '.jsx, .tsx' ./components --out-dir ./build"
}
}
Lari:
npm run build
Sekarang, kita sekarang memiliki 2 file Button.js, Dropdown.js di direktori build, di dalam file tersebut ada vanilla js yang dirancang dengan baik + beberapa polyfills dan lonely requre ("styles.scss") . Jelas, ini tidak akan berfungsi dalam demo, hapus impor gaya (pada saat itu saya menggerogoti harapan bahwa saya akan menemukan plugin untuk transpile scss), dan mengumpulkannya lagi.
Setelah perakitan, kami masih memiliki beberapa JS yang bagus. Mari coba lagi untuk mengintegrasikan komponen yang dirakit ke dalam styleguidist:
```javascript
import Button from '../../build/Button'
<Button></Button>
```
Disusun - berhasil. Hanya tombol tanpa gaya.
Kami mencari plugin untuk transpile scss / sass
Ya, perakitan komponen berfungsi, komponen berfungsi, Anda dapat membangun, menerbitkan di npm atau nexus kerja Anda sendiri. Tetap, simpan saja gayanya ... Ok, Google akan membantu kami lagi (tidak).
Googling plugin belum memberi saya hasil apa pun. Satu plugin menghasilkan string dari gaya, yang lain tidak berfungsi sama sekali dan bahkan perlu mengimpor view: import styles dari "styles.scss"
Satu-satunya harapan untuk plugin ini: babel-plugin-transform-scss-import-to-string, tapi itu hanya menghasilkan garis dari gaya (ah ... sudah saya katakan di atas. Sial ...). Kemudian semuanya menjadi lebih buruk, saya mencapai halaman 6 di Google (dan jam sudah menunjukkan jam 3 pagi). Dan tidak akan ada pilihan khusus untuk menemukan sesuatu. Ya, dan tidak ada yang perlu dipikirkan - baik webpack + sass-loader, yang melakukannya dengan buruk dan bukan untuk kasus saya, atau SESUATU LAIN. Saraf ... Saya memutuskan untuk istirahat, minum teh, saya masih belum mau tidur. Ketika saya sedang membuat teh, ide untuk menulis sebuah plugin untuk transpile scss / sass terus muncul di kepala saya. Sementara gula diaduk, suara sendok di kepala saya berbunyi: "Tulis plaagin." Oke, putuskan, saya akan menulis plugin.
Plugin tidak ditemukan. Kami menulis sendiri
Saya menggunakan babel-plugin-transform-scss-import-to-string yang disebutkan di atas sebagai dasar untuk plugin saya . Saya sangat mengerti bahwa sekarang akan ada wasir dengan pohon AST, dan trik lainnya. Oke, ayo pergi.
Kami membuat persiapan awal. Kami membutuhkan node-sass dan path, serta baris reguler untuk file dan ekstensi. Idenya adalah ini:
- Kami mendapatkan jalur ke file dengan gaya dari garis impor
- Parse gaya menjadi string melalui node-sass (berkat babel-plugin-transform-scss-import-to-string)
- Kami membuat tag gaya untuk setiap impor (plugin babel diluncurkan pada setiap impor)
- Penting untuk mengidentifikasi gaya yang dibuat, agar tidak melemparkan hal yang sama pada setiap bersin hot-reload. Mari masukkan beberapa atribut (data-sass-component) dengan nilai file saat ini dan nama stylesheet. Akan ada yang seperti ini:
<style data-sass-component="Button_style"> .button { display: flex; } </style>
Untuk mengembangkan plugin dan mengujinya di proyek, pada level dengan direktori komponen, saya membuat direktori babel-plugin-transform-scss, memasukkan package.json di sana dan memasukkan direktori lib di sana, dan saya sudah memasukkan index.js ke dalamnya.
Apa jadinya Anda vkurse - Konfigurasi Babel naik di belakang plugin, yang ditentukan dalam direktif utama di package.json, untuk ini saya harus menjejalkannya.Kami menunjukkan:
{
//...package.json - , main
main: "lib/index.js"
}
Kemudian, dorong jalur ke plugin ke dalam babel config (.babelrc):
{
//
plugins: [
"./babel-plugin-transform-scss"
//
]
}
Sekarang, mari menjejalkan beberapa keajaiban ke index.js.
Tahap pertama adalah memeriksa impor file scss atau sass, mendapatkan nama file yang diimpor, mendapatkan nama file js (komponen) itu sendiri, memindahkan string scss atau sass ke css. Kami memotong WebStorm ke npm menjalankan build melalui debugger, mengatur breakpoint, melihat argumen jalur dan status, dan mencari nama file, memprosesnya dengan kutukan:
const { resolve, dirname, join } = require("path");
const { renderSync } = require("node-sass");
const regexps = {
sassFile: /([A-Za-z0-9]+).s[ac]ss/g,
sassExt: /\.s[ac]ss$/,
currentFile: /([A-Za-z0-9]+).(t|j)s(x)/g,
currentFileExt: /.(t|j)s(x)/g
};
function transformScss(babel) {
const { types: t } = babel;
return {
name: "babel-plugin-transform-scss",
visitor: {
ImportDeclaration(path, state) {
/**
* , scss/sass
*/
if (!regexps.sassExt.test(path.node.source.value)) return;
const sassFileNameMatch = path.node.source.value.match(
regexps.sassFile
);
/**
* scss/sass js
*/
const sassFileName = sassFileNameMatch[0].replace(regexps.sassExt, "");
const file = this.filename.match(regexps.currentFile);
const filename = `${file[0].replace(
regexps.currentFileExt,
""
)}_${sassFileName}`;
/**
*
* scss/sass , css
*/
const scssFileDirectory = resolve(dirname(state.file.opts.filename));
const fullScssFilePath = join(
scssFileDirectory,
path.node.source.value
);
const projectRoot = process.cwd();
const nodeModulesPath = join(projectRoot, "node_modules");
const sassDefaults = {
file: fullScssFilePath,
sourceMap: false,
includePaths: [nodeModulesPath, scssFileDirectory, projectRoot]
};
const sassResult = renderSync({ ...sassDefaults, ...state.opts });
const transpiledContent = sassResult.css.toString() || "";
}
}
}
Api. Sukses pertama, dapatkan baris css di transpiledContent. Selanjutnya, hal terburuk - kami naik ke babeljs.io/docs/en/babel-types#api untuk API di pohon AST. Kami masuk ke astexplorer.net dan menulis kode untuk mendorong stylesheet ke head.
Di astexplorer.net, tulis fungsi Self-Invoking yang akan dipanggil di tempat impor gaya:
(function(){
const styles = "generated transpiledContent" // ".button {/n display: flex; /n}/n"
const fileName = "generated_attributeValue" //Button_style
const element = document.querySelector("style[data-sass-component='fileName']")
if(!element){
const styleBlock = document.createElement("style")
styleBlock.innerHTML = styles
styleBlock.setAttribute("data-sass-component", fileName)
document.head.appendChild(styleBlock)
}
})()
Di penjelajah AST, poke di sisi kiri pada baris, deklarasi, literal, - di sebelah kanan pohon kita melihat struktur deklarasi, kita naik ke babeljs.io/docs/en/babel-types#api menggunakan struktur ini , mengasapi semua ini dan menulis penggantinya.
Beberapa saat kemudian ...
1-1.5 jam kemudian, berjalan melalui tab dari ast ke api jenis babel, lalu ke dalam kode, saya menulis pengganti impor scss / sass. Saya tidak akan mengurai pohon ast dan api jenis babel secara terpisah, bahkan akan ada lebih banyak huruf. Saya langsung tunjukkan hasilnya:
const { resolve, dirname, join } = require("path");
const { renderSync } = require("node-sass");
const regexps = {
sassFile: /([A-Za-z0-9]+).s[ac]ss/g,
sassExt: /\.s[ac]ss$/,
currentFile: /([A-Za-z0-9]+).(t|j)s(x)/g,
currentFileExt: /.(t|j)s(x)/g
};
function transformScss(babel) {
const { types: t } = babel;
return {
name: "babel-plugin-transform-scss",
visitor: {
ImportDeclaration(path, state) {
/**
* , scss/sass
*/
if (!regexps.sassExt.test(path.node.source.value)) return;
const sassFileNameMatch = path.node.source.value.match(
regexps.sassFile
);
/**
* scss/sass js
*/
const sassFileName = sassFileNameMatch[0].replace(regexps.sassExt, "");
const file = this.filename.match(regexps.currentFile);
const filename = `${file[0].replace(
regexps.currentFileExt,
""
)}_${sassFileName}`;
/**
*
* scss/sass , css
*/
const scssFileDirectory = resolve(dirname(state.file.opts.filename));
const fullScssFilePath = join(
scssFileDirectory,
path.node.source.value
);
const projectRoot = process.cwd();
const nodeModulesPath = join(projectRoot, "node_modules");
const sassDefaults = {
file: fullScssFilePath,
sourceMap: false,
includePaths: [nodeModulesPath, scssFileDirectory, projectRoot]
};
const sassResult = renderSync({ ...sassDefaults, ...state.opts });
const transpiledContent = sassResult.css.toString() || "";
/**
* , AST Explorer
* replaceWith path.
*/
path.replaceWith(
t.callExpression(
t.functionExpression(
t.identifier(""),
[],
t.blockStatement(
[
t.variableDeclaration("const", [
t.variableDeclarator(
t.identifier("styles"),
t.stringLiteral(transpiledContent)
)
]),
t.variableDeclaration("const", [
t.variableDeclarator(
t.identifier("fileName"),
t.stringLiteral(filename)
)
]),
t.variableDeclaration("const", [
t.variableDeclarator(
t.identifier("element"),
t.callExpression(
t.memberExpression(
t.identifier("document"),
t.identifier("querySelector")
),
[
t.stringLiteral(
`style[data-sass-component='${filename}']`
)
]
)
)
]),
t.ifStatement(
t.unaryExpression("!", t.identifier("element"), true),
t.blockStatement(
[
t.variableDeclaration("const", [
t.variableDeclarator(
t.identifier("styleBlock"),
t.callExpression(
t.memberExpression(
t.identifier("document"),
t.identifier("createElement")
),
[t.stringLiteral("style")]
)
)
]),
t.expressionStatement(
t.assignmentExpression(
"=",
t.memberExpression(
t.identifier("styleBlock"),
t.identifier("innerHTML")
),
t.identifier("styles")
)
),
t.expressionStatement(
t.callExpression(
t.memberExpression(
t.identifier("styleBlock"),
t.identifier("setAttribute")
),
[
t.stringLiteral("data-sass-component"),
t.identifier("fileName")
]
)
),
t.expressionStatement(
t.callExpression(
t.memberExpression(
t.memberExpression(
t.identifier("document"),
t.identifier("head"),
false
),
t.identifier("appendChild"),
false
),
[t.identifier("styleBlock")]
)
)
],
[]
),
null
)
],
[]
),
false,
false
),
[]
)
);
}
}
}
Sukacita terakhir
Hore !!! Impor diganti dengan panggilan ke fungsi yang menjejalkan gaya dengan tombol ini ke bagian atas dokumen. Dan kemudian saya berpikir, bagaimana jika saya memulai seluruh kayak ini melalui webpack, memotong sass-loader? Akankah ini berhasil? Oke, kita potong dan periksa. Saya meluncurkan perakitan dengan webpack, menunggu kesalahan yang harus saya tentukan loader untuk jenis file ini ... Tetapi tidak ada kesalahan, semuanya sudah terpasang. Saya membuka halaman, melihat, dan gaya menempel di kepala dokumen. Menariknya, saya juga menyingkirkan 3 pemuat gaya (senyum sangat bahagia).
Jika Anda tertarik dengan artikel tersebut, harap dukung dengan tanda bintang di github .
Ada juga tautan ke paket npm: www.npmjs.com/package/babel-plugin-transform-scss
Catatan: Di luar artikel, tambahkan tanda centang untuk mengimpor gaya menurut jenisimpor gaya dari './styles.scss'