摘要:下面一步步拆解上述流程。切換至分支檢測本地和暫存區是否還有未提交的文件檢測本地分支是否有誤檢測本地分支是否落后遠程分支發布發布檢測到在分支上沒有沖突后,立即執行。
背景
最近一直在著手做一個與業務強相關的組件庫,一直在思考要從哪里下手,怎么來設計這個組件庫,因為業務上一直在使用ElementUI(以下簡稱Element),于是想參考了一下Element組件庫的設計,看看Element構建方式,并且總結成了這篇文章。
Element的目錄結構廢話不多說,先看看目錄結構,從目錄結構入手,一步步進行分解。
├─build // 構建相關的腳本和配置 ├─examples // 用于展示Element組件的demo ├─lib // 構建后生成的文件,發布到npm包 ├─packages // 組件代碼 ├─src // 引入組件的入口文件 ├─test // 測試代碼 ├─Makefile // 構建文件 ├─components.json // 組件列表 └─package.json有哪些構建命令
剛打開的時候看到了一個Makefile文件,如果學過c/c++的同學對這個東西應該不陌生,當時看到后臺同學發布版本時,寫下了一句make love,把我和我的小伙伴們都驚呆了。說正經的,makefile可以說是比較早出現在UNIX 系統中的工程化工具,通過一個簡單的make XXX來執行一系列的編譯和鏈接操作。不懂makefile文件的可以看這篇文章了解下:前端入門->makefile
當我們打開Element的Makefile時,發現里面的操作都是npm script的命令,我不知道為什么還要引入Makefile,直接使用npm run xxx就好了呀。
default: help install: npm install new: node build/bin/new.js $(filter-out $@,$(MAKECMDGOALS)) dev: npm run dev deploy: @npm run deploy dist: install npm run dist pub: npm run pub help: @echo "make 命令使用說明" @echo "make install --- 安裝依賴" @echo "make new開發模式與構建入口文件[中文名] --- 創建新組件 package. 例如 "make new button 按鈕"" @echo "make dev --- 開發模式" @echo "make dist --- 編譯項目,生成目標文件" @echo "make deploy --- 部署 demo" @echo "make pub --- 發布到 npm 上" @echo "make new-lang --- 為網站添加新語言. 例如 "make new-lang fr""
這里我們只挑選幾個重要的看看。首先看到make install,使用的是npm進行依賴安裝,但是Element實際上是使用yarn進行依賴管理,所以如果你要在本地進行Element開發的話,最好使用yarn進行依賴安裝。在官方的貢獻指南也有提到。
同時在package.json文件中有個bootstrap命令就是使用yarn來安裝依賴。
"bootstrap": "yarn || npm i",
安裝完依賴之后,就可以進行開發了,運行npm run dev,可以通過webpack-dev-sever在本地運行Element官網的demo。
"dev": " npm run bootstrap && // 依賴安裝 npm run build:file && // 目標文件生成 cross-env NODE_ENV=development webpack-dev-server --config build/webpack.demo.js & node build/bin/template.js " "build:file": " node build/bin/iconInit.js & // 解析icon.scss,將所有小圖標的name存入examples/icon.json node build/bin/build-entry.js & // 根據components.json,生成入口文件 node build/bin/i18n.js & // 根據examples/i18n/page.json和模板,生成不同語言的demo node build/bin/version.js // 生成examples/versions.json,鍵值對,各個大版本號對應的最新版本 "
在通過webpack-dev-server運行demo時,有個前置條件,就是通過npm run build:file生成目標文件。這里主要看下node build/bin/build-entry.js,這個腳本用于生成Element的入口js。先是讀取根目錄的components.json,這個json文件維護著Element的所有的組件名,鍵為組件名,值為組件源碼的入口文件;然后遍歷鍵值,將所有組件進行import,對外暴露install方法,把所有import的組件通過Vue.component(name, component)方式注冊為全局組件,并且把一些彈窗類的組件掛載到Vue的原型鏈上。具體代碼如下(ps:對代碼進行一些精簡,具體邏輯不變):
var Components = require("../../components.json"); var fs = require("fs"); var render = require("json-templater/string"); var uppercamelcase = require("uppercamelcase"); var path = require("path"); var endOfLine = require("os").EOL; // 換行符 var includeComponentTemplate = []; var installTemplate = []; var listTemplate = []; Object.keys(Components).forEach(name => { var componentName = uppercamelcase(name); //將組件名轉為駝峰 var componetPath = Components[name] includeComponentTemplate.push(`import ${componentName} from ".${componetPath}";`); // 這幾個特殊組件不能直接注冊成全局組件,需要掛載到Vue的原型鏈上 if (["Loading", "MessageBox", "Notification", "Message"].indexOf(componentName) === -1) { installTemplate.push(` ${componentName}`); } if (componentName !== "Loading") listTemplate.push(` ${componentName}`); }); var template = `/* Automatically generated by "./build/bin/build-entry.js" */ ${includeComponentTemplate.join(endOfLine)} import locale from "element-ui/src/locale"; import CollapseTransition from "element-ui/src/transitions/collapse-transition"; const components = [ ${installTemplate.join("," + endOfLine)}, CollapseTransition ]; const install = function(Vue, opts = {}) { locale.use(opts.locale); locale.i18n(opts.i18n); components.forEach(component => { Vue.component(component.name, component); }); Vue.use(Loading.directive); Vue.prototype.$ELEMENT = { size: opts.size || "", zIndex: opts.zIndex || 2000 }; Vue.prototype.$loading = Loading.service; Vue.prototype.$msgbox = MessageBox; Vue.prototype.$alert = MessageBox.alert; Vue.prototype.$confirm = MessageBox.confirm; Vue.prototype.$prompt = MessageBox.prompt; Vue.prototype.$notify = Notification; Vue.prototype.$message = Message; }; /* istanbul ignore if */ if (typeof window !== "undefined" && window.Vue) { install(window.Vue); } module.exports = { version: "${process.env.VERSION || require("../../package.json").version}", locale: locale.use, i18n: locale.i18n, install, CollapseTransition, Loading, ${listTemplate.join("," + endOfLine)} }; module.exports.default = module.exports; `; // 寫文件 fs.writeFileSync(OUTPUT_PATH, template); console.log("[build entry] DONE:", OUTPUT_PATH);
最后生成的代碼如下:
/* Automatically generated by "./build/bin/build-entry.js" */ import Button from "../packages/button/index.js"; import Table from "../packages/table/index.js"; import Form from "../packages/form/index.js"; import Row from "../packages/row/index.js"; import Col from "../packages/col/index.js"; // some others Component import locale from "element-ui/src/locale"; import CollapseTransition from "element-ui/src/transitions/collapse-transition"; const components = [ Button, Table, Form, Row, Menu, Col, // some others Component ]; const install = function(Vue, opts = {}) { locale.use(opts.locale); locale.i18n(opts.i18n); components.forEach(component => { Vue.component(component.name, component); }); Vue.use(Loading.directive); Vue.prototype.$ELEMENT = { size: opts.size || "", zIndex: opts.zIndex || 2000 }; Vue.prototype.$loading = Loading.service; Vue.prototype.$msgbox = MessageBox; Vue.prototype.$alert = MessageBox.alert; Vue.prototype.$confirm = MessageBox.confirm; Vue.prototype.$prompt = MessageBox.prompt; Vue.prototype.$notify = Notification; Vue.prototype.$message = Message; }; /* istanbul ignore if */ if (typeof window !== "undefined" && window.Vue) { install(window.Vue); } module.exports = { version: "2.4.6", locale: locale.use, i18n: locale.i18n, install, Button, Table, Form, Row, Menu, Col, // some others Component }; module.exports.default = module.exports;
最后有個寫法需要注意:module.exports.default = module.exports;,這里是為了兼容ESmodule,因為es6的模塊export default xxx,在webpack中最后會變成類似于exports.default = xxx的形式,而import ElementUI from "element-ui";會變成ElementUI = require("element-ui").default的形式,為了讓ESmodule識別這種commonjs的寫法,就需要加上default。
exports對外暴露的install方法就是把Element組件注冊會全局組件的方法。當我們使用Vue.use時,就會調用對外暴露的install方法。如果我們直接通過script的方式引入vue和Element,檢測到Vue為全局變量時,也會調用install方法。
// 使用方式1 // 使用方式2 import Vue from "vue"; import ElementUI from "element-ui"; import "element-ui/lib/theme-chalk/index.css"; Vue.use(ElementUI); // 此時會調用ElementUI.install()
在module.exports對象中,除了暴露install方法外,還把所有組件進行了對外的暴露,方便引入單個組件。
import { Button } from "element-ui"; Vue.use(Button);
但是如果你有進行按需加載,使用Element官方的babel-plugin-component插件,上面代碼會轉換成如下形式:
var _button = require("element-ui/lib/button") require("element-ui/lib/theme-chalk/button.css") Vue.use(_button)
那么前面module.exports對外暴露的單組件好像也沒什么用。
不過這里使用npm run build:file生成文件的方式是可取的,因為在實際項目中,我們每新增一個組件,只需要修改components.json文件,然后使用npm run build:file重新生成代碼就可以了,不需要手動去修改多個文件。
在生成了入口文件的index.js之后就會運行webpack-dev-server。
webpack-dev-server --config build/webpack.demo.js
接下來看下webpack.demo.js的入口文件:
// webpack.demo.js const webpackConfig = { entry: "./examples/entry.js", output: { path: path.resolve(process.cwd(), "./examples/element-ui/"), publicPath: process.env.CI_ENV || "", filename: "[name].[hash:7].js", chunkFilename: isProd ? "[name].[hash:7].js" : "[name].js" }, resolve: { extensions: [".js", ".vue", ".json"], alias: { main: path.resolve(__dirname, "../src"), packages: path.resolve(__dirname, "../packages"), examples: path.resolve(__dirname, "../examples"), "element-ui": path.resolve(__dirname, "../") }, modules: ["node_modules"] } // ... some other config } // examples/entry.js import Vue from "vue"; import Element from "main/index.js"; Vue.use(Element);新建組件
entry.js就是直接引入的之前build:file中生成的index.js的Element的入口文件。因為這篇文章主要講構建流程,所以不會仔細看demo的源碼。下面看看Element如何新建一個組件,在Makefile可以看到使用make new xxx新建一個組件。。
new: node build/bin/new.js $(filter-out $@,$(MAKECMDGOALS))
這后面的$(filter-out $@,$(MAKECMDGOALS))就是把命令行輸入的參數直接傳輸給node build/bin/new.js,具體細節這里不展開,還是直接看看build/bin/new.js的具體細節。
// 參數校驗 if (!process.argv[2]) { console.error("[組件名]必填 - Please enter new component name"); process.exit(1); } const path = require("path"); const fileSave = require("file-save"); const uppercamelcase = require("uppercamelcase"); // 獲取命令行的參數 // e.g. node new.js input 輸入框 // process.argv表示命令行的參數數組 // 0是node,1是new.js,2和3就是后面兩個參數 const componentname = process.argv[2]; // 組件名 const chineseName = process.argv[3] || componentname; const ComponentName = uppercamelcase(componentname); // 轉成駝峰表示 // 組件所在的目錄文件 const PackagePath = path.resolve(__dirname, "../../packages", componentname); // 檢查components.json中是否已經存在同名組件 const componentsFile = require("../../components.json"); if (componentsFile[componentname]) { console.error(`${componentname} 已存在.`); process.exit(1); } // componentsFile中寫入新的組件鍵值對 componentsFile[componentname] = `./packages/${componentname}/index.js`; fileSave(path.join(__dirname, "../../components.json")) .write(JSON.stringify(componentsFile, null, " "), "utf8") .end(" "); const Files = [ { filename: "index.js", content: `index.js相關模板` }, { filename: "src/main.vue", content: `組件相關的模板` }, // 下面三個文件是的對應的中英文api文檔 { filename: path.join("../../examples/docs/zh-CN", `${componentname}.md`), content: `## ${ComponentName} ${chineseName}` }, { filename: path.join("../../examples/docs/en-US", `${componentname}.md`), content: `## ${ComponentName}` }, { filename: path.join("../../examples/docs/es", `${componentname}.md`), content: `## ${ComponentName}` }, { filename: path.join("../../test/unit/specs", `${componentname}.spec.js`), content: `組件相關測試用例的模板` }, { filename: path.join("../../packages/theme-chalk/src", `${componentname}.scss`), content: `組件的樣式文件` }, { filename: path.join("../../types", `${componentname}.d.ts`), content: `組件的types文件,用于語法提示` } ]; // 生成組件必要的文件 Files.forEach(file => { fileSave(path.join(PackagePath, file.filename)) .write(file.content, "utf8") .end(" "); });
這個腳本最終會在components.json寫入組件相關的鍵值對,同時在packages目錄創建對應的組件文件,并在packages/theme-chalk/src目錄下創建一個樣式文件,Element的樣式是使用sass進行預編譯的,所以生成是.scss文件。大致看下packages目錄下生成的文件的模板:
{ filename: "index.js", content: ` import ${ComponentName} from "./src/main"; /* istanbul ignore next */ ${ComponentName}.install = function(Vue) { Vue.component(${ComponentName}.name, ${ComponentName}); }; export default ${ComponentName}; ` }, { filename: "src/main.vue", content: ` ` }
每個組件都會對外多帶帶暴露一個install方法,因為Element支持按需加載。同時,每個組件名都會加上El前綴。,所以我們使用Element組件時,經常是這樣的el-xxx,這符合W3C的自定義HTML標簽的規范(小寫,并且包含一個短杠)。
打包流程由于現代前端的復雜環境,代碼寫好之后并不能直接使用,被拆成模塊的代碼,需要通過打包工具進行打包成一個多帶帶的js文件。并且由于各種瀏覽器的兼容性問題,還需要把ES6語法轉譯為ES5,sass、less等css預編譯語言需要經過編譯生成瀏覽器真正能夠運行的css文件。所以,當我們通過npm run new component新建一個組件,并通過npm run dev在本地調試好代碼后,需要把進行打包操作,才能真正發布到npm上。
這里運行npm run dist進行Element的打包操作,具體命令如下。
"dist": " npm run clean && npm run build:file && npm run lint && webpack --config build/webpack.conf.js && webpack --config build/webpack.common.js && webpack --config build/webpack.component.js && npm run build:utils && npm run build:umd && npm run build:theme "
下面一步步拆解上述流程。
清理文件"clean": "rimraf lib && rimraf packages/*/lib && rimraf test/**/coverage"
使用npm run clean會刪除之前打包生成的文件,這里直接使用了一個node包:rimraf,類似于linux下的rm -rf。
入口文件生成npm run build:file在前面已經介紹過了,通過components.json生成入口文件。
代碼檢查"lint": "eslint src/**/* test/**/* packages/**/* build/**/* --quiet"
使用ESLint對多個目錄下的文件進行lint操作。
文件打包webpack --config build/webpack.conf.js && webpack --config build/webpack.common.js && webpack --config build/webpack.component.js &&
這里直接使用原生webpack進行打包操作,webpack版本為:3.7.1。在Element@2.4.0之前,使用的打包工具為cooking,但是這個工具是基于webpack2,很久沒有更新(ps. 項目中能使用webpack最好使用webpack,多閱讀官網的文檔,雖然文檔很爛,其他第三方對webpack進行包裝的構建工具,很容易突然就不更新了,到時候要遷移會很麻煩)。
這三個配置文件的配置基本類似,區別在entry和output。
// webpack.conf.js module.exports = { entry: { app: ["./src/index.js"] }, output: { path: path.resolve(process.cwd(), "./lib"), publicPath: "/dist/", filename: "index.js", chunkFilename: "[id].js", libraryTarget: "umd", library: "ELEMENT", umdNamedDefine: true } } // webpack.common.js module.exports = { entry: { app: ["./src/index.js"] }, output: { path: path.resolve(process.cwd(), "./lib"), publicPath: "/dist/", filename: "element-ui.common.js", chunkFilename: "[id].js", libraryTarget: "commonjs2" } } // webpack.component.js const Components = require("../components.json"); module.exports = { entry: Components, output: { path: path.resolve(process.cwd(), "./lib"), publicPath: "/dist/", filename: "[name].js", chunkFilename: "[id].js", libraryTarget: "commonjs2" } }
webpack.conf.js 與 webpack.common.js打包的入口文件都是src/index.js,該文件通過npm run build:file生成。不同之處在于輸出文件,兩個配置生成的js都在lib目錄,重點在于libraryTarget,一個是umd,一個是commonjs2。還一個 webpack.component.js 的入口文件為 components.json 中的所有組件,表示packages目錄下的所有組件都會在lib文件夾下生成也多帶帶的js文件,這些組件多帶帶的js文件就是用來做按需加載的,如果需要哪個組件,就會多帶帶import這個組件js。
當我們直接在代碼中引入整個Element的時候,加載的是 webpack.common.js 打包生成的 element-ui.common.js 文件。因為我們引入npm包的時候,會根據package.json中的main字段來查找入口文件。
// package.json "main": "lib/element-ui.common.js"轉譯工具方法
"build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js",
這一部分是吧src目錄下的除了index.js入口文件外的其他文件通過babel轉譯,然后移動到lib文件夾下。
└─src ├─directives ├─locale ├─mixins ├─transitions ├─popup └─index.js
在src目錄下,除了index.js外,還有一些其他文件夾,這些是Element組件中經常使用的工具方法。如果你對Element的源碼足夠熟悉,可以直接把Element中一些工具方法拿來使用,不再需要安裝其他的包。
const date = require("element-ui/lib/utils/date") date.format(new Date, "HH:mm:ss")生成樣式文件
"build:theme": " node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk "
這里直接使用gulp將scss文件轉為css文件。
gulp.src("./src/*.scss") .pipe(sass.sync()) .pipe(autoprefixer({ browsers: ["ie > 9", "last 2 versions"], cascade: false })) .pipe(cssmin()) .pipe(gulp.dest("./lib"));
最終我們引入的element-ui/lib/theme-chalk/index.css,其源文件只不過是把所有組件的scss文件進行import。這個index.scss是在運行gulp之前,通過node build/bin/gen-cssfile命令生成的,邏輯與生成js的入口文件類似,同樣是遍歷components.json。
發布流程代碼經過之前的編譯,就到了發布流程,在Element中發布主要是用shell腳本實現的。Element發布一共涉及三個部分。
git發布
npm發布
官網發布
// 新版本發布 "pub": " npm run bootstrap && sh build/git-release.sh && sh build/release.sh && node build/bin/gen-indices.js && sh build/deploy-faas.sh "git沖突檢測
運行 git-release.sh 進行git沖突的檢測,這里主要是檢測dev分支是否沖突,因為Element是在dev分支進行開發的(這個才Element官方的開發指南也有提到),只有在最后發布時,才merge到master。
#!/usr/bin/env sh # 切換至dev分支 git checkout dev # 檢測本地和暫存區是否還有未提交的文件 if test -n "$(git status --porcelain)"; then echo "Unclean working tree. Commit or stash changes first." >&2; exit 128; fi # 檢測本地分支是否有誤 if ! git fetch --quiet 2>/dev/null; then echo "There was a problem fetching your branch. Run `git fetch` to see more..." >&2; exit 128; fi # 檢測本地分支是否落后遠程分支 if test "0" != "$(git rev-list --count --left-only @"{u}"...HEAD)"; then echo "Remote history differ. Please pull changes." >&2; exit 128; fi echo "No conflicts." >&2;git發布;npm發布
檢測到git在dev分支上沒有沖突后,立即執行release.sh。
這一部分代碼比較簡單,可以直接在github上查看。上述發布流程,省略了一個部分,就是Element會將其樣式也發布到npm上。
# publish theme echo "Releasing theme-chalk $VERSION ..." cd packages/theme-chalk npm version $VERSION --message "[release] $VERSION" if [[ $VERSION =~ "beta" ]] then npm publish --tag beta else npm publish fi
如果你只想使用Element的樣式,不使用它的Vue組件,你也可以直接在npm上下載他們的樣式,不過一般也沒人這么做吧。
npm install -S element-theme-chalk官網更新
這一步就不詳細說了,因為不在文章想說的構建流程之列。
大致就是將靜態資源生成到examples/element-ui目錄下,然后放到gh-pages分支,這樣就能通過github pages的方式訪問。不信,你訪問試試。
http://elemefe.github.io/element
同時在該分支下,寫入了CNAME文件,這樣訪問element.eleme.io也能定向到element的github pages了。
echo element.eleme.io>>examples/element-ui/CNAME總結
Element的代碼總體看下來,還是十分流暢的,對自己做組件化幫助很大。剛開始寫這篇文章的時候,標題寫著主流組件庫的構建流程,想把Element和antd的構建流程都寫出來,寫完Element才發現這個坑開得好大,于是麻溜的把標題改成Element的構建流程。當然Element除了其構建流程,本身很多組件的實現思路也很優雅,大家感興趣可以去看一看。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/97836.html
摘要:最近項目中做表格比較多,對表格的使用,只需要傳遞進去數據,然后寫死表頭即可渲染。該函數可以返回一個包含兩個元素的數組,第一個元素代表,第二個元素代表。也可以返回一個鍵名為和的對象。 最近項目中做表格比較多,對element表格的使用,只需要傳遞進去數據,然后寫死表頭即可渲染。 但現實中應用中,如果寫死表頭,并且每個組件中寫自己的表格,不僅浪費時間而且消耗性能。這個時候需要動態渲染表頭。...
摘要:在實際開發過程中發現,考試系統各個表集合都是需要關聯,這種非關系型數據庫,做起來反而麻煩了不少。數據中既有試卷的信息,也有很多題目。題目都屬于該試卷,改試卷又屬于當前登錄系統的老師即創建試卷的老師。 這是我畢業項目,從0到1,前后臺獨立開發完成。功能不多,在此記錄,溫故而知新!項目github地址:https://github.com/FinGet/Exam ,博客地址:https:/...
摘要:基于這種思路,那留給我們的只有兩步,組件設計和數據設計。關于組件的相關邏輯,可能要在文章里面一次性說清楚,還是需要費很大的精力,不過希望數據驅動的思想能夠讓之前沒有體會到這種開發樂趣的小伙伴們有到新的想法。 在日常開發中,我們肯定不止一次碰到重復的業務代碼,明明功能相似,但總沒思路去把它封裝成組件。關于封裝組件,希望這篇文章能帶給大家新的思路,去更高效的完成日常開發。(注:例子都是基于...
摘要:使用基于依賴追蹤的觀察并且使用異步隊列更新。為項目配置文件。為項目靜態資源目錄。其實個人感覺通用項目目錄可以很隨意的搭配,比如說之后清空目錄封裝通用組件,像是啊,滑動常用組件。 寫在前面的個人體會,大神們可以跳過 這段時間接手一個后臺管理項目,從最開始寫一點我自己的體會吧。首先Vue,Angular和React是當今主流前端三大框架。Vue是一個用來構建網頁的JS庫,相比較Angula...
閱讀 3723·2021-10-13 09:39
閱讀 3789·2021-09-24 09:48
閱讀 1189·2021-09-01 10:30
閱讀 2526·2019-08-30 15:55
閱讀 1774·2019-08-29 16:39
閱讀 2296·2019-08-26 13:55
閱讀 3050·2019-08-26 12:23
閱讀 1634·2019-08-26 11:59