The article has been included in github , welcome to Watch and Star.
brief introduction
The source code architecture of ElementUI is explained in detail, which lays a solid foundation for building the team's own component library based on ElementUI in the next step.
How to quickly build your own component library for the team?
Component library is an indispensable infrastructure in the field of modern front-end. It can improve the reusability and maintainability of the code, improve the production efficiency of the team, and better serve the future.
So how to build your own component library for the team? The most ideal solution is to use the ability of the community to cut an excellent open source library, and only keep what you need, such as its architecture, engineering and documentation capabilities, as well as some basic components. In the process of cutting, you may find some problems in it, and then optimize and solve them in your component library.
Element source code architecture
Because the technical stack of the team is Vue, we choose to carry out secondary development based on element. Before starting, we first analyze the source code of element framework in detail, so as to make knowledge reserve for building component library. Element framework source code consists of five parts: Engineering, official website, component library, test and type declaration.
Engineering
The architecture of element is really excellent. It realizes excellent engineering through a large number of scripts, and is committed to making the developers of component library focus on the thing itself. For example, when adding a new component, you can generate all the files of the component with one click and complete the preparation of the basic structure of these files and the related import configuration. A total of 13 files are involved in the addition and change, and you only need to complete the component definition. The engineering of element consists of five parts: the engineering configuration and script under the build directory, eslint, travis ci, Makefile and package JSON scripts.
build
The build directory stores engineering related configurations and scripts. For example, the JS script in the / build/bin directory allows the component library developers to focus on the development of components. In addition, they don't need to care about anything else; Build / MD loader is the key to realize component demo + document according to markdown on the component page of the official website; There are also continuous integration, webpack configuration, etc. next, these configurations and scripts will be introduced in detail.
/build/bin/build-entry.js
The component configuration file (components.json) combines with the string template library to automatically generate / SRC / index JS file to avoid manually adding components in / SRC / index JS.
/** * Generate / SRC / index js * 1,Automatically import all components of the component library * 2,Define the install method of fully registered component library components * 3,Export version, install and components */ // key is the package name and path is the value var Components = require('../../components.json'); var fs = require('fs'); // Template library var render = require('json-templater/string'); // Convert name comp to comp var uppercamelcase = require('uppercamelcase'); var path = require('path'); var endOfLine = require('os').EOL; // Output path / SRC / index js var OUTPUT_PATH = path.join(__dirname, '../../src/index.js'); // Import template, import compname from '/ packages/comp-name/index. js' var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\';'; // ' CompName' var INSTALL_COMPONENT_TEMPLATE = ' {{name}}'; // /src/index.js template var MAIN_TEMPLATE = `/* Automatically generated by './build/bin/build-entry.js' */ {{include}} import locale from 'element-ui/src/locale'; import CollapseTransition from 'element-ui/src/transitions/collapse-transition'; const components = [ {{install}}, CollapseTransition ]; const install = function(Vue, opts = {}) { locale.use(opts.locale); locale.i18n(opts.i18n); components.forEach(component => { Vue.component(component.name, component); }); Vue.use(InfiniteScroll); 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); } export default { version: '{{version}}', locale: locale.use, i18n: locale.i18n, install, CollapseTransition, Loading, {{list}} }; `; delete Components.font; // Get all package names, [comp-name1, comp-name2] var ComponentNames = Object.keys(Components); // Store all import statements var includeComponentTemplate = []; // Component name array var installTemplate = []; // Component name array var listTemplate = []; // Traverse all package names ComponentNames.forEach(name => { // Convert the package name in hyphen format into the form of large hump, which is the component name, such as form item = "FormItem" var componentName = uppercamelcase(name); // Replace the template variable in the import statement and generate the import statement, import fromitem from '/ packages/form-item/index. js' includeComponentTemplate.push(render(IMPORT_TEMPLATE, { name: componentName, package: name })); // These components are removed from the components array without global registration. They are attached to the prototype chain and written in the install method of the template string if (['Loading', 'MessageBox', 'Notification', 'Message', 'InfiniteScroll'].indexOf(componentName) === -1) { installTemplate.push(render(INSTALL_COMPONENT_TEMPLATE, { name: componentName, component: name })); } // Put all components into listTemplates and export them at last if (componentName !== 'Loading') listTemplate.push(` ${componentName}`); }); // Replace the four variables in the template var template = render(MAIN_TEMPLATE, { include: includeComponentTemplate.join(endOfLine), install: installTemplate.join(',' + endOfLine), version: process.env.VERSION || require('../../package.json').version, list: listTemplate.join(',' + endOfLine) }); // Write the ready template to / SRC / index js fs.writeFileSync(OUTPUT_PATH, template); console.log('[build entry] DONE:', OUTPUT_PATH);
/build/bin/build-locale.js
Translate all ES Module style translation files (/ src/locale/lang) into UMD style through babel.
/** * Translate ES Module style translation files into UMD style through babel */ var fs = require('fs'); var save = require('file-save'); var resolve = require('path').resolve; var basename = require('path').basename; // Directory of translation documents, which are used on the official website var localePath = resolve(__dirname, '../../src/locale/lang'); // Get all translation files in the directory var fileList = fs.readdirSync(localePath); // Conversion function var transform = function(filename, name, cb) { require('babel-core').transformFile(resolve(localePath, filename), { plugins: [ 'add-module-exports', ['transform-es2015-modules-umd', {loose: true}] ], moduleId: name }, cb); }; // Traverse all files fileList // Only js files are processed. In fact, there are no non js files in the directory .filter(function(file) { return /\.js$/.test(file); }) .forEach(function(file) { var name = basename(file, '.js'); // Call the conversion function and write the converted code to the lib/umd/locale directory transform(file, name, function(err, result) { if (err) { console.error(err); } else { var code = result.code; code = code .replace('define(\'', 'define(\'element/locale/') .replace('global.', 'global.ELEMENT.lang = global.ELEMENT.lang || {}; \n global.ELEMENT.lang.'); save(resolve(__dirname, '../../lib/umd/locale', file)).write(code); console.log(file); } }); });
/build/bin/gen-cssfile.js
Automatically in / packages / theme chat / SRC / index The style of each component package is introduced into scss|css. This style file is required when registering the component library in full, that is, import 'packages / theme chat / SRC / index scss.
/** * Automatically in / packages / theme chat / SRC / index The style of each component package is introduced into SCSS | CSS * This style file is required when registering the component library in full, that is, import 'packages / theme chat / SRC / index scss */ var fs = require('fs'); var path = require('path'); var Components = require('../../components.json'); var themes = [ 'theme-chalk' ]; // Get all package names Components = Object.keys(Components); // Basic path of all component packages, / packages var basepath = path.resolve(__dirname, '../../packages/'); // Judge whether the specified file exists function fileExists(filePath) { try { return fs.statSync(filePath).isFile(); } catch (err) { return false; } } // Traverse all component packages, generate import statements that introduce all component package styles, and then automatically generate packages / theme chalk / SRC / index SCSS CSS file themes.forEach((theme) => { // Whether it is scss or not. Element UI uses scss writing style by default var isSCSS = theme !== 'theme-default'; // Import the basic style file @ import "./base.scss|css";\n var indexContent = isSCSS ? '@import "./base.scss";\n' : '@import "./base.css";\n'; // Traverse all component packages and generate @ import ". / comp package. Scss|css"; \n Components.forEach(function(key) { // Skip these three component packages if (['icon', 'option', 'option-group'].indexOf(key) > -1) return; // comp-package.scss|css var fileName = key + (isSCSS ? '.scss' : '.css'); // Import statement, @ import ". / comp package. Scss|css"; \n indexContent += '@import "./' + fileName + '";\n'; // If the style file of the component package does not exist, such as / packages / form item / theme talk / SRC / form item If SCSS does not exist, it is considered to be missing and the file is created var filePath = path.resolve(basepath, theme, 'src', fileName); if (!fileExists(filePath)) { fs.writeFileSync(filePath, '', 'utf8'); console.log(theme, ' Create missing ', fileName, ' file'); } }); // Generate / packages / theme chat / SRC / index SCSS | CSS is responsible for introducing the styles of all component packages fs.writeFileSync(path.resolve(basepath, theme, 'src', isSCSS ? 'index.scss' : 'index.css'), indexContent); });
/build/bin/i18n.js
Generate official website pages in four languages according to the template (/ examples/pages/template) vue file.
'use strict'; var fs = require('fs'); var path = require('path'); // The official website page translation configuration is built-in in four languages var langConfig = require('../../examples/i18n/page.json'); // Traverse all languages langConfig.forEach(lang => { // Create / examples/pages/{lang}, for example: / examples / pages / zh cn try { fs.statSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`)); } catch (e) { fs.mkdirSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`)); } // Traverse all pages according to page TPL automatically generates the corresponding language vue file Object.keys(lang.pages).forEach(page => { // For example, / examples / pages / template / index tpl var templatePath = path.resolve(__dirname, `../../examples/pages/template/${ page }.tpl`); // /examples/pages/zh-CN/index.vue var outputPath = path.resolve(__dirname, `../../examples/pages/${ lang.lang }/${ page }.vue`); // Read template file var content = fs.readFileSync(templatePath, 'utf8'); // Read the configuration of all key value pairs on the index page var pairs = lang.pages[page]; // Traverse these key value pairs and replace the corresponding key in the template by regular matching Object.keys(pairs).forEach(key => { content = content.replace(new RegExp(`<%=\\s*${ key }\\s*>`, 'g'), pairs[key]); }); // Write the replaced contents to vue file fs.writeFileSync(outputPath, content); }); });
/build/bin/iconInit.js
According to icon The selector in SCSS style file matches all icon names through regular matching, then forms an array of these icon names, and writes the array to / examples / icon JSON file, which is used to automatically generate all icon icons on the icon icon page of the official website.
'use strict'; /** * According to icon The selector in the SCSS style file matches all icon names through regular matching, * Then write an array of all icon names to / examples / icon JSON file * This file is used to automatically generate all icon icons on the icon page of the official website */ var postcss = require('postcss'); var fs = require('fs'); var path = require('path'); // icon.scss file content var fontFile = fs.readFileSync(path.resolve(__dirname, '../../packages/theme-chalk/src/icon.scss'), 'utf8'); // Get style node var nodes = postcss.parse(fontFile).nodes; var classList = []; // Traverse all style nodes nodes.forEach((node) => { // Match the icon name from the selector, such as El icon add, and get add var selector = node.selector || ''; var reg = new RegExp(/\.el-icon-([^:]+):before/); var arr = selector.match(reg); // Write the icon name to the array, if (arr && arr[1]) { classList.push(arr[1]); } }); classList.reverse(); // You want to sort the css files in reverse order // Write an array of icon names to / examples / icon JSON file fs.writeFile(path.resolve(__dirname, '../../examples/icon.json'), JSON.stringify(classList), () => {});
/build/bin/new-lang.js
Add a new language to the component library, such as fr (French). Set the relevant configuration of the language for the involved files (components.json, page.json, route.json, nav.config.json and docs). The specific configuration items are English by default. You only need to translate these English configuration items into the corresponding language in the corresponding files.
'use strict'; /** * Add a new language to the component library, such as fr (French) * Set the relevant configuration of the language for the involved files (components.json, page.json, route.json, nav.config.json, docs) * The specific configuration items are in English by default. You only need to translate these English configuration items into the corresponding language in the corresponding file */ console.log(); process.on('exit', () => { console.log(); }); if (!process.argv[2]) { console.error('[language] is required!'); process.exit(1); } var fs = require('fs'); const path = require('path'); const fileSave = require('file-save'); const lang = process.argv[2]; // const configPath = path.resolve(__dirname, '../../examples/i18n', lang); // Add to components json const componentFile = require('../../examples/i18n/component.json'); if (componentFile.some(item => item.lang === lang)) { console.error(`${lang} already exists.`); process.exit(1); } let componentNew = Object.assign({}, componentFile.filter(item => item.lang === 'en-US')[0], { lang }); componentFile.push(componentNew); fileSave(path.join(__dirname, '../../examples/i18n/component.json')) .write(JSON.stringify(componentFile, null, ' '), 'utf8') .end('\n'); // Add to page json const pageFile = require('../../examples/i18n/page.json'); // The default configuration of the new language is English. You only need to go to page JSON to translate what should be in the language configuration into this language let pageNew = Object.assign({}, pageFile.filter(item => item.lang === 'en-US')[0], { lang }); pageFile.push(pageNew); fileSave(path.join(__dirname, '../../examples/i18n/page.json')) .write(JSON.stringify(pageFile, null, ' '), 'utf8') .end('\n'); // Add to route json const routeFile = require('../../examples/i18n/route.json'); routeFile.push({ lang }); fileSave(path.join(__dirname, '../../examples/i18n/route.json')) .write(JSON.stringify(routeFile, null, ' '), 'utf8') .end('\n'); // Add to NAV config. json const navFile = require('../../examples/nav.config.json'); navFile[lang] = navFile['en-US']; fileSave(path.join(__dirname, '../../examples/nav.config.json')) .write(JSON.stringify(navFile, null, ' '), 'utf8') .end('\n'); // Create a new folder under docs try { fs.statSync(path.resolve(__dirname, `../../examples/docs/${ lang }`)); } catch (e) { fs.mkdirSync(path.resolve(__dirname, `../../examples/docs/${ lang }`)); } console.log('DONE!');
/build/bin/new.js
When adding new components to the component library, the script will be used to generate all component files with one click, and complete the preparation of the basic structure of these files and related import configuration. A total of 13 files will be added and changed, such as make new city city list. The existence of this script allows you to focus on the writing of component code when developing new components for the component library, and leave everything else alone.
'use strict'; /** * Add new component * For example: make new city list * 1,Create a new component directory under / packages directory and complete the creation of directory structure * 2,Create a component document, / examples / docs / {Lang} / city md * 3,Create a component unit test file, / test / unit / specs / city spec.js * 4,Create a component style file, / packages / theme chat / SRC / city scss * 5,Create a component type declaration file, / types / city d.ts * 6,to configure * At / components Configuration component information in JSON file * At / examples / NAV config. Add the routing configuration of this component in JSON * In / packages / theme chat / SRC / index The style file of this component is automatically imported into the SCSS file * Place the type declaration file in / types / element UI d. Automatic introduction in TS * In short, the existence of the script allows you to focus on writing your component code and leave everything else alone */ console.log(); process.on('exit', () => { console.log(); }); if (!process.argv[2]) { console.error('[Component name]Required - Please enter new component name'); process.exit(1); } const path = require('path'); const fs = require('fs'); const fileSave = require('file-save'); const uppercamelcase = require('uppercamelcase'); // Component name, such as city const componentname = process.argv[2]; // Chinese name of the component const chineseName = process.argv[3] || componentname; // Convert the component name to the form of large hump, city = > City const ComponentName = uppercamelcase(componentname); // Component package directory, / packages/city const PackagePath = path.resolve(__dirname, '../../packages', componentname); // List of files to be added and basic structure of file content const Files = [ // /packages/city/index.js { filename: 'index.js', // File content, introduce components, define component static method, install is used to register components, and then export components content: `import ${ComponentName} from './src/main'; /* istanbul ignore next */ ${ComponentName}.install = function(Vue) { Vue.component(${ComponentName}.name, ${ComponentName}); }; export default ${ComponentName};` }, // Define the basic structure of components, / packages / city / SRC / main vue { filename: 'src/main.vue', // File content, sfc content: `<template> <div class="el-${componentname}"></div> </template> <script> export default { name: 'El${ComponentName}' }; </script>` }, // Documents in four languages, / examples / docs / {Lang} / city MD and set the file title { 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('../../examples/docs/fr-FR', `${componentname}.md`), content: `## ${ComponentName}` }, // Component test file, / test / unit / specs / city spec.js { filename: path.join('../../test/unit/specs', `${componentname}.spec.js`), // The basic structure of the test file is given content: `import { createTest, destroyVM } from '../util'; import ${ComponentName} from 'packages/${componentname}'; describe('${ComponentName}', () => { let vm; afterEach(() => { destroyVM(vm); }); it('create', () => { vm = createTest(${ComponentName}, true); expect(vm.$el).to.exist; }); }); ` }, // Component style file, / packages / theme chat / SRC / city scss { filename: path.join('../../packages/theme-chalk/src', `${componentname}.scss`), // Basic structure of documents content: `@import "mixins/mixins"; @import "common/var"; @include b(${componentname}) { }` }, // Component type declaration file { filename: path.join('../../types', `${componentname}.d.ts`), // Basic structure of type declaration file content: `import { ElementUIComponent } from './component' /** ${ComponentName} Component */ export declare class El${ComponentName} extends ElementUIComponent { }` } ]; // Add components to components json,{ City: './packages/city/index.js' } const componentsFile = require('../../components.json'); if (componentsFile[componentname]) { console.error(`${componentname} Already exists.`); process.exit(1); } componentsFile[componentname] = `./packages/${componentname}/index.js`; fileSave(path.join(__dirname, '../../components.json')) .write(JSON.stringify(componentsFile, null, ' '), 'utf8') .end('\n'); // Set the component style file in index Introduction into SCSS const sassPath = path.join(__dirname, '../../packages/theme-chalk/src/index.scss'); const sassImportText = `${fs.readFileSync(sassPath)}@import "./${componentname}.scss";`; fileSave(sassPath) .write(sassImportText, 'utf8') .end('\n'); // Place the type declaration file of the component in element UI d. Introduction into TS const elementTsPath = path.join(__dirname, '../../types/element-ui.d.ts'); let elementTsText = `${fs.readFileSync(elementTsPath)} /** ${ComponentName} Component */ export class ${ComponentName} extends El${ComponentName} {}`; const index = elementTsText.indexOf('export') - 1; const importString = `import { El${ComponentName} } from './${componentname}'`; elementTsText = elementTsText.slice(0, index) + importString + '\n' + elementTsText.slice(index); fileSave(elementTsPath) .write(elementTsText, 'utf8') .end('\n'); // Traverse the Files array, create all the listed Files and write the file contents Files.forEach(file => { fileSave(path.join(PackagePath, file.filename)) .write(file.content, 'utf8') .end('\n'); }); // In nav config. Add the routing configuration corresponding to the new component in JSON const navConfigFile = require('../../examples/nav.config.json'); // Traverse each language in the configuration, and add the routing configuration of the component to all language configurations Object.keys(navConfigFile).forEach(lang => { let groups = navConfigFile[lang][4].groups; groups[groups.length - 1].list.push({ path: `/${componentname}`, title: lang === 'zh-CN' && componentname !== chineseName ? `${ComponentName} ${chineseName}` : ComponentName }); }); fileSave(path.join(__dirname, '../../examples/nav.config.json')) .write(JSON.stringify(navConfigFile, null, ' '), 'utf8') .end('\n'); console.log('DONE!');
One disadvantage here is that / SRC / index. Is not automatically regenerated when creating a new component JS, that is, the newly generated components will not be automatically introduced into the component library entry. This is also simple. Just configure Makefile and change the new command to node build / bin / new JS $(filter out $@, $(makecmdgoals)) & & NPM run build: file.
/build/bin/template.js
Listen to all template files in / examples/pages/template directory. When the template file changes, npm run i18n will be executed automatically, that is, I18N JS script, regenerate the four languages vue file.
/** * Listen to all template files in / examples/pages/template directory, and automatically execute npm run i18n when the template file changes, * Execute I18N JS script, regenerate the four languages vue file */ const path = require('path'); // Listening directory const templates = path.resolve(process.cwd(), './examples/pages/template'); // Library responsible for monitoring const chokidar = require('chokidar'); // Listening template directory let watcher = chokidar.watch([templates]); // When the files in the directory are changed, npm run i18n is automatically executed watcher.on('ready', function() { watcher .on('change', function() { exec('npm run i18n'); }); }); // Responsible for executing orders function exec(cmd) { return require('child_process').execSync(cmd).toString().trim(); }
/build/bin/version.js
According to / package JSON file, automatically generate / examples / version JSON, which is used to record the version information of the component library. These versions will be used in the navigation bar at the head of the component page of the official website.
/** * According to package JSON auto generate / examples / version JSON, which is used to record the version information of the component library * These version information will be used in the head navigation bar of the component page of the official website */ var fs = require('fs'); var path = require('path'); var version = process.env.VERSION || require('../../package.json').version; var content = { '1.4.13': '1.4', '2.0.11': '2.0', '2.1.0': '2.1', '2.2.2': '2.2', '2.3.9': '2.3', '2.4.11': '2.4', '2.5.4': '2.5', '2.6.3': '2.6', '2.7.2': '2.7', '2.8.2': '2.8', '2.9.2': '2.9', '2.10.1': '2.10', '2.11.1': '2.11', '2.12.0': '2.12', '2.13.2': '2.13', '2.14.1': '2.14' }; if (!content[version]) content[version] = '2.15'; fs.writeFileSync(path.resolve(__dirname, '../../examples/versions.json'), JSON.stringify(content));
/build/md-loader
It is a loader, which is responsible for more than half of the credit of the component demo + document mode of the component page of the official website.
It can be found in / examples / route config. JS, use the loadDocs method to load / examples / docs / {Lang} / comp md . Note that the markdown document loaded here is not the usual Vue file, but it can be rendered as a Vue component on the page like a Vue file. How is this done?
As we know, the concept of webpack is that all resources can be require d, just configure the corresponding loader. At / build / webpack demo. JS file You can see the processing of markdow (. md) rules under rules. First process the markdown file through MD loader, parse the vue code from it, and then give it to vue loader. Finally, sfc (vue single file component) is generated and rendered to the page. You can see the document + component demo display effect of the component page.
{ test: /\.md$/, use: [ { loader: 'vue-loader', options: { compilerOptions: { preserveWhitespace: false } } }, { loader: path.resolve(__dirname, './md-loader/index.js') } ] }
If you are interested in the specific implementation of loader, you can read it in depth by yourself.
/build/config.js
Public configuration of webpack, such as externals, alias, etc. Through the configuration of externals, the redundancy of some codes of component library is solved, such as the codes of components and common modules of component library, but the redundancy of component style has not been solved; Alias alias configuration provides convenience for developing component library.
/** * webpack Public configuration, such as externals and alias */ var path = require('path'); var fs = require('fs'); var nodeExternals = require('webpack-node-externals'); var Components = require('../components.json'); var utilsList = fs.readdirSync(path.resolve(__dirname, '../src/utils')); var mixinsList = fs.readdirSync(path.resolve(__dirname, '../src/mixins')); var transitionList = fs.readdirSync(path.resolve(__dirname, '../src/transitions')); /** * externals Solve the problem of code redundancy when components depend on other components and are introduced on demand * For example, the Table component depends on the Checkbox component. Will redundant code be generated if I introduce both Table and Checkbox in the project * If you don't have the following content, you will see two Checkbox component codes at this time. * Redundant code will also appear, including public contents such as locale, utils, mixins and transitions * However, with the setting of externals, it will tell webpack that it is not necessary to package these import ed packages into bundle s, and then go from the outside at runtime * Get these extension dependencies. This allows you to / lib / tables. After packaging JS to see the compiled table JS dependency on Checkbox component introduction: * module.exports = require("element-ui/lib/checkbox") * After this processing, there will be no redundant JS code, but for the CSS part, the element UI does not deal with redundancy. * You can see / lib / theme chalk / table CSS and / lib / theme chalk / Checkbox CSS has the style of Checkbox component */ var externals = {}; Object.keys(Components).forEach(function(key) { externals[`element-ui/packages/${key}`] = `element-ui/lib/${key}`; }); externals['element-ui/src/locale'] = 'element-ui/lib/locale'; utilsList.forEach(function(file) { file = path.basename(file, '.js'); externals[`element-ui/src/utils/${file}`] = `element-ui/lib/utils/${file}`; }); mixinsList.forEach(function(file) { file = path.basename(file, '.js'); externals[`element-ui/src/mixins/${file}`] = `element-ui/lib/mixins/${file}`; }); transitionList.forEach(function(file) { file = path.basename(file, '.js'); externals[`element-ui/src/transitions/${file}`] = `element-ui/lib/transitions/${file}`; }); externals = [Object.assign({ vue: 'vue' }, externals), nodeExternals()]; exports.externals = externals; // Set alias for easy use exports.alias = { main: path.resolve(__dirname, '../src'), packages: path.resolve(__dirname, '../packages'), examples: path.resolve(__dirname, '../examples'), 'element-ui': path.resolve(__dirname, '../') }; exports.vue = { root: 'Vue', commonjs: 'vue', commonjs2: 'vue', amd: 'vue' }; exports.jsexclude = /node_modules|utils\/popper\.js|utils\/date\.js/;
/build/deploy-ci.sh
Continuous integration script used in combination with travis ci. This script is in travis. The code is executed in the YML file. After the code is submitted to the github warehouse, it will be automatically executed by Tavis CI, and ci will automatically find the code in the project travis.yml file and execute the commands inside. But we may not need this. Generally, the team will have its own continuous integration scheme.
/build/git-release.sh
Here, we mainly diff and merge with the remote dev branch.
#!/usr/bin/env sh # Here, we mainly diff and merge with the remote dev branch 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;
/build/release.sh
The script does the following:
- Merge dev branches to master
- Modify the version number of style packs and component libraries
- Publish style packs and component libraries
- Submit master and dev branches to remote warehouse
This script can be used when publishing component libraries, especially the function of automatically changing the version number (forgetting to change the version number every time you publish). It is simple to submit the code to the log of the remote warehouse. For more details, update the log file changelog {lang}. Provided by MD.
#!/usr/bin/env sh set -e # Merge dev branches to master # Compilation and packaging # Modify the version number of style packs and component libraries # Publish style packs and component libraries # Submit master and dev branches to remote warehouse # Merge dev branches to master git checkout master git merge dev # Version selection cli VERSION=`npx select-version-cli` # Confirm current version information read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r echo # (optional) move to a new line if [[ $REPLY =~ ^[Yy]$ ]] then echo "Releasing $VERSION ..." # build, compile and package VERSION=$VERSION npm run dist # ssr test node test/ssr/require.test.js # publish theme echo "Releasing theme-chalk $VERSION ..." cd packages/theme-chalk # Change the version information of the theme pack npm version $VERSION --message "[release] $VERSION" # Publish theme if [[ $VERSION =~ "beta" ]] then npm publish --tag beta else npm publish fi cd ../.. # commit git add -A git commit -m "[build] $VERSION" # Change the version information of the component library npm version $VERSION --message "[release] $VERSION" # publish to push the master to the remote warehouse git push eleme master git push eleme refs/tags/v$VERSION git checkout dev git rebase master git push eleme dev # Publish component library if [[ $VERSION =~ "beta" ]] then npm publish --tag beta else npm publish fi fi
/build/webpack.xx.js
- webpack.common.js, build the package of commonjs2 specification, and will make a full package
webpack.component.js, build the package of commonjs2 specification, and support on-demand loading
Supporting on-demand loading focuses on the configuration of entry and output, and each component is packaged separately
- webpack.conf.js, build the package of UMD specification, and make a full package
- webpack.demo.js, webpack configuration of official website project
- webpack.extension.js, the webpack configuration of the chorme plug-in project of the theme editor. The project is under the extension directory
- webpack.test.js, this file is of no use, but look at the name. It should be used to test the webpack configuration of the project, but now the test uses the karma framework
eslint
Element ensures the consistency of code style through eslint, and specially writes elemefe as the extended rule configuration of eslint. In order to ensure the quality of the official website project, in / build / webpack demo. The eslint loader rule is configured in JS to force the check of code quality when the project starts. However, element has not done enough in code quality control. For example, the ability of automatic code formatting is too weak, only the code quality under / src, / test, / packages and / build directories is guaranteed, and the official website project is not done enough, especially the limitation of document format. Here, we suggest that you integrate a prettier specifically to limit the format, and let eslint focus on the limitation of code syntax. You can refer to it Build your own typescript project + develop your own scaffolding tool TS cli To configure the code quality part of.
travis ci
travis ci combines scripts to complete continuous integration. However, this may not be useful for internal projects, because travis ci can only be used for github. gitlab is generally used internally, and there is also supporting continuous integration
Makefile
The configuration file of make command should be familiar to students who have written C and C + +.
You can see detailed help information by executing the make command. For example, execute make install package, make dev to start the local development environment, make new comp name to create new components, etc. Using the make command is more convenient, clear and simple than npm run xx, but it also relies on npm run xx to complete the real work internally. It is equivalent to providing a layer of encapsulation for many NPM run CMDS for better development experience.
package.json -> scripts
elemnt has written many npm scripts. These scripts are combined with many scripts in / build to automatically complete a large number of repeated physical labor through scripts, which is more reliable and efficient than manual work. I think this design is the most worthy place in element. You can apply this design to your own projects to help improve business efficiency.
{ // Pack "bootstrap": "yarn || npm i", // Through JS script, the following files are automatically generated: examples / icon JSON file & & generate Src / index JS file & & generates the official website of four languages vue file & & generate examples / version JSON file, which contains the version information of the component library "build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js", // Build theme style: in index Automatically introduce the style files of each component into scss & & compile the scss file into css through gulp and output it to lib Directory & & copy the basic style theme chalk to lib / theme chalk "build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk", // Compile the src directory through babel, and then output the compiled files to the lib directory, ignoring / src / index js "build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js", // Compile ES Module style translation files into UMD style "build:umd": "node build/bin/build-locale.js", // Clear build products "clean": "rimraf lib && rimraf packages/*/lib && rimraf test/**/coverage", // Build official website project "deploy:build": "npm run build:file && cross-env NODE_ENV=production webpack --config build/webpack.demo.js && echo element.eleme.io>>examples/element-ui/CNAME", // Building theme plug-ins "deploy:extension": "cross-env NODE_ENV=production webpack --config build/webpack.extension.js", // Start the development environment of the theme plug-in "dev:extension": "rimraf examples/extension/dist && cross-env NODE_ENV=development webpack --watch --config build/webpack.extension.js", // Start the local development environment of the component library. Execute build:file to automatically generate some files & & start the example project, that is, the official website & & monitor the changes of all template files in the examples/pages/template directory, and regenerate if they are changed vue", "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", // Component test items, in examples / play / index Any component of the component library can be introduced into Vue, or you can directly use the project started by dev to use components in the document "dev:play": "npm run build:file && cross-env NODE_ENV=development PLAY_ENV=true webpack-dev-server --config build/webpack.demo.js", // Build component library "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", // Generate official websites in four languages vue file "i18n": "node build/bin/i18n.js", // lint to ensure the quality of project code "lint": "eslint src/**/* test/**/* packages/**/* build/**/* --quiet", // Package & & Merge dev branch of remote warehouse & & Merge dev branch to master, package and compile, modify the version number of style package and component library, publish style package and component library, and submit code to remote warehouse. Note out the last script when using. There is a problem with that script "pub": "npm run bootstrap && sh build/git-release.sh && sh build/release.sh && node build/bin/gen-indices.js", // Generating test reports, whether test or test:watch, takes too long to generate a test report "test": "npm run lint && npm run build:theme && cross-env CI_ENV=/dev/ BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", // Start the test project to detect the update of the test file "test:watch": "npm run build:theme && cross-env BABEL_ENV=test karma start test/unit/karma.conf.js" }
Official website
The official website of element is in the same warehouse as the component library. Everything on the official website is placed in the / examples directory, which is a vue project.
entry.js
The entrance to the official website project, where you can fully introduce the component library and its styles.
// The entrance of the official website project is an ordinary vue project import Vue from 'vue'; import entry from './app'; import VueRouter from 'vue-router'; // Introduce the component library. main is the alias, which is in / build / config Configuration in JS import Element from 'main/index.js'; import hljs from 'highlight.js'; // Routing configuration import routes from './route.config'; // Some components of the official website project import demoBlock from './components/demo-block'; import MainFooter from './components/footer'; import MainHeader from './components/header'; import SideNav from './components/side-nav'; import FooterNav from './components/footer-nav'; import title from './i18n/title'; // Component library style import 'packages/theme-chalk/src/index.scss'; import './demo-styles/index.scss'; import './assets/styles/common.css'; import './assets/styles/fonts/style.css'; // The icon information is attached to the Vue prototype chain and used in the markdown document. All icon icons are displayed on the icon icon page of the official website import icon from './icon.json'; Vue.use(Element); Vue.use(VueRouter); Vue.component('demo-block', demoBlock); Vue.component('main-footer', MainFooter); Vue.component('main-header', MainHeader); Vue.component('side-nav', SideNav); Vue.component('footer-nav', FooterNav); const globalEle = new Vue({ data: { $isEle: false } // ele user }); Vue.mixin({ computed: { $isEle: { get: () => (globalEle.$data.$isEle), set: (data) => {globalEle.$data.$isEle = data;} } } }); Vue.prototype.$icon = icon; // Icon list page const router = new VueRouter({ mode: 'hash', base: __dirname, routes }); router.afterEach(route => { // https://github.com/highlightjs/highlight.js/issues/909#issuecomment-131686186 Vue.nextTick(() => { const blocks = document.querySelectorAll('pre code:not(.hljs)'); Array.prototype.forEach.call(blocks, hljs.highlightBlock); }); const data = title[route.meta.lang]; for (let val in data) { if (new RegExp('^' + val, 'g').test(route.name)) { document.title = data[val]; return; } } document.title = 'Element'; ga('send', 'event', 'PageView', route.name); }); new Vue({ // eslint-disable-line ...entry, router }).$mount('#app');
nav.config.json
For the configuration of the side navigation bar of the official website component page, you must understand the structure of the json file before you can understand route config. JS file to generate the code of all routes of the component page.
route.config.js
Automatically generate the route configuration of the official website project according to the route configuration.
// Automatically generate the route of the official website project according to the route configuration import navConfig from './nav.config'; // All supported languages import langs from './i18n/route'; // Load all pages of the official website vue file const LOAD_MAP = { 'zh-CN': name => { return r => require.ensure([], () => r(require(`./pages/zh-CN/${name}.vue`)), 'zh-CN'); }, 'en-US': name => { return r => require.ensure([], () => r(require(`./pages/en-US/${name}.vue`)), 'en-US'); }, 'es': name => { return r => require.ensure([], () => r(require(`./pages/es/${name}.vue`)), 'es'); }, 'fr-FR': name => { return r => require.ensure([], () => r(require(`./pages/fr-FR/${name}.vue`)), 'fr-FR'); } }; const load = function(lang, path) { return LOAD_MAP[lang](path); }; // Load the markdown files of each component on the component page of the official website const LOAD_DOCS_MAP = { 'zh-CN': path => { return r => require.ensure([], () => r(require(`./docs/zh-CN${path}.md`)), 'zh-CN'); }, 'en-US': path => { return r => require.ensure([], () => r(require(`./docs/en-US${path}.md`)), 'en-US'); }, 'es': path => { return r => require.ensure([], () => r(require(`./docs/es${path}.md`)), 'es'); }, 'fr-FR': path => { return r => require.ensure([], () => r(require(`./docs/fr-FR${path}.md`)), 'fr-FR'); } }; const loadDocs = function(lang, path) { return LOAD_DOCS_MAP[lang](path); }; // Add the routing configuration of the component page. To understand the following code, you must understand NAV config. JSON file structure const registerRoute = (navConfig) => { let route = []; // Traverse the configuration and generate component routing configuration in four languages Object.keys(navConfig).forEach((lang, index) => { // The configuration of the specified language, such as lang = zh CN, navs means that all configuration items are written in Chinese let navs = navConfig[lang]; // Routing configuration of lang language on component page route.push({ // For example: / zh CN / component path: `/${ lang }/component`, redirect: `/${ lang }/component/installation`, // Load the component of the component page vue component: load(lang, 'component'), // All sub routes of the component page, that is, all components, are placed here. The last route is / zh CN / component / comp path children: [] }); // Traverses all configuration items in the specified language navs.forEach(nav => { if (nav.href) return; if (nav.groups) { // This item is a component nav.groups.forEach(group => { group.list.forEach(nav => { addRoute(nav, lang, index); }); }); } else if (nav.children) { // This is a development guide nav.children.forEach(nav => { addRoute(nav, lang, index); }); } else { // Others, such as update log, Element React, Element Angular addRoute(nav, lang, index); } }); }); // Generate sub route configuration and fill it in children function addRoute(page, lang, index) { // Decide whether to load vue file or markdown file according to the path const component = page.path === '/changelog' ? load(lang, 'changelog') : loadDocs(lang, page.path); let child = { path: page.path.slice(1), meta: { title: page.title || page.name, description: page.description, lang }, name: 'component-' + lang + (page.title || page.name), component: component.default || component }; // Add the sub route to the children above route[index].children.push(child); } return route; }; // Get the routing configuration of all sidebars of the component page let route = registerRoute(navConfig); const generateMiscRoutes = function(lang) { let guideRoute = { path: `/${ lang }/guide`, // guide redirect: `/${ lang }/guide/design`, component: load(lang, 'guide'), children: [{ path: 'design', // Design principles name: 'guide-design' + lang, meta: { lang }, component: load(lang, 'design') }, { path: 'nav', // Navigation name: 'guide-nav' + lang, meta: { lang }, component: load(lang, 'nav') }] }; let themeRoute = { path: `/${ lang }/theme`, component: load(lang, 'theme-nav'), children: [ { path: '/', // Theme management name: 'theme' + lang, meta: { lang }, component: load(lang, 'theme') }, { path: 'preview', // Theme preview editing name: 'theme-preview-' + lang, meta: { lang }, component: load(lang, 'theme-preview') }] }; let resourceRoute = { path: `/${ lang }/resource`, // resources meta: { lang }, name: 'resource' + lang, component: load(lang, 'resource') }; let indexRoute = { path: `/${ lang }`, // home page meta: { lang }, name: 'home' + lang, component: load(lang, 'index') }; return [guideRoute, resourceRoute, themeRoute, indexRoute]; }; langs.forEach(lang => { route = route.concat(generateMiscRoutes(lang.lang)); }); route.push({ path: '/play', name: 'play', component: require('./play/index.vue') }); let userLanguage = localStorage.getItem('ELEMENT_LANGUAGE') || window.navigator.language || 'en-US'; let defaultPath = '/en-US'; if (userLanguage.indexOf('zh-') !== -1) { defaultPath = '/zh-CN'; } else if (userLanguage.indexOf('es') !== -1) { defaultPath = '/es'; } else if (userLanguage.indexOf('fr') !== -1) { defaultPath = '/fr-FR'; } route = route.concat([{ path: '/', redirect: defaultPath }, { path: '*', redirect: defaultPath }]); export default route;
play
Including play And index.js/play Vue, an example item. For example, if you want to see the effect of a component in an element, especially the display effect when the component is loaded on demand, you can see it in play / index Use is introduced into Vue, and the npm run dev:play command is used to start the project, which is also in / build / webpack demo. JS is configured through environment variables.
// play.js import Vue from 'vue'; // Full introduction of component library and its style import Element from 'main/index.js'; import 'packages/theme-chalk/src/index.scss'; import App from './play/index.vue'; Vue.use(Element); new Vue({ // eslint-disable-line render: h => h(App) }).$mount('#app');
<!-- play/index.vue --> <template> <div style="margin: 20px;"> <el-input v-model="input" placeholder="Please enter the content"></el-input> </div> </template> <script> export default { data() { return { input: 'Hello Element UI!' }; } }; </script>
pages
All pages of the official website are here, through I18N JS script combines various template files in the pages/template directory to automatically generate templates in four languages in the pages directory vue files, which will be displayed in route config. JS.
i18n
The translation configuration files of the official website page are here.
- component.json, translation configuration of component page
- page.json, some translation configurations of other pages, such as home page, design page, etc
- route.json, language configuration, indicates which languages are currently supported by the component library
- theme-editor.json, translation configuration of topic editor page
- title.json, the title information displayed in the tab of each page of the official website
extension
The theme editor's chrome plug-in project.
dom
The dom style operation methods are defined, including judging whether the specified style exists, adding style, removing style and switching style.
// dom/class.js export const hasClass = function(obj, cls) { return obj.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')); }; export const addClass = function(obj, cls) { if (!hasClass(obj, cls)) obj.className += ' ' + cls; }; export const removeClass = function(obj, cls) { if (hasClass(obj, cls)) { const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)'); obj.className = obj.className.replace(reg, ' '); } }; export const toggleClass = function(obj, cls) { if (hasClass(obj, cls)) { removeClass(obj, cls); } else { addClass(obj, cls); } };
docs
Component document directory, which provides documents in four languages by default. The directory structure is docs / {Lang} / comp name md. These documents are loaded on the component page (configured in route.config.js). First, they are handed over to the MD loader for processing, the vue code is extracted, and then handed over to the vue loader for processing. Finally, they are rendered to the page to form the component demo + document.
demo-style
The layout style of the component demo displayed on the component page has nothing to do with the style of the component itself, just as you define the layout style of the component in your business code. Because the direct display effect of components is not good in some scenarios, it needs to go through certain typesetting, such as button page, icon page, etc.
components
The official website project stores directories of some global components.
assets
Static resource directory of official website project
Component library
The element component library consists of two parts: / src and / packages.
src
Using the modular development idea, put some public modules that the component depends on in the / src directory, and split the following modules according to their functions:
- utils, which defines some tools and methods
- transitions, animation
- mixins, some methods of global mixing
- locale, internationalization function and translation files of some components in various languages
- directives
/src/index.js is through the script / build / bin / build entry JS script is generated automatically and is the entrance of component library. It is responsible for automatically importing all components of the component library, defining the install method of fully registering the components of the component library, and then exporting version information, install and various components.
/* Pass'/ build/bin/build-entry.js' file automatically generated */ // Import all components import Pagination from '../packages/pagination/index.js'; import Dialog from '../packages/dialog/index.js'; // ... // Component array. Some components are not in it. These components do not need to pass Vue Use or Vue Registered as a component and directly attached to the Vue prototype chain const components = [ Pagination, Dialog, // ... ] // Define the install method, which is responsible for importing all components into the component library const install = function(Vue, opts = {}) { locale.use(opts.locale); locale.i18n(opts.i18n); // Global registration component components.forEach(component => { Vue.component(component.name, component); }); Vue.use(InfiniteScroll); Vue.use(Loading.directive); // Hang something on the Vue prototype chain Vue.prototype.$ELEMENT = { size: opts.size || '', zIndex: opts.zIndex || 2000 }; // These components do not require 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; }; // When importing a component library through CDN, follow the following code to fully register the component library if (typeof window !== 'undefined' && window.Vue) { install(window.Vue); } // Export version information, install method and components export default { version: '2.15.0', locale: locale.use, i18n: locale.i18n, install, CollapseTransition, Loading, // ... }
In order to reduce the length, only a part of the document is posted, but it is enough to explain everything.
/packages
element puts all the components in the / packages directory. Each component is based on the directory. The directory structure and the basic code in it are through the script / build / bin / new JS automatically generated. The directory structure is:
Package name, package name in hyphen form
- index.js, the install method of the component, which indicates that the component exists in the form of Vue plug-in
src, source directory of components
- main. The basic structure of the Vue component is ready
For example, the directory and files of the newly created city component are as follows:
city
index.js
import City from './src/main'; /* istanbul ignore next */ City.install = function(Vue) { Vue.component(City.name, City); }; export default City;
src
main.vue
<template> <div class="el-city"></div> </template> <script> export default { name: 'ElCity' }; </script>
In fact, in the / packages directory, in addition to components, there is a special directory, theme chat, which is the style directory of the component library. The style codes of all components are here. There is no style defined in the component file of element. The theme chalk directory is also a project. It is packaged through gulp and supports independent publishing. Its directory structure is as follows:
theme-chalk
src, source directory of component style
- index.scss, import all style files in the directory
- comp.scss, component style file, for example: button scss
- other, such as font, public style, variable, method, etc
- .gitignore
gulpfile.js
'use strict'; // gulp configuration file const { series, src, dest } = require('gulp'); const sass = require('gulp-sass'); const autoprefixer = require('gulp-autoprefixer'); const cssmin = require('gulp-cssmin'); // scss is compiled into css and compressed, and finally output to/ lib directory function compile() { return src('./src/*.scss') .pipe(sass.sync()) .pipe(autoprefixer({ browsers: ['ie > 9', 'last 2 versions'], cascade: false })) .pipe(cssmin()) .pipe(dest('./lib')); } // Copy/ src/fonts to/ lib/fonts function copyfont() { return src('./src/fonts/**') .pipe(cssmin()) .pipe(dest('./lib/fonts')); } exports.build = series(compile, copyfont);
- package.json
- README.md
test
The test project of component library uses the karma framework
Type declaration
The type declaration file of each component. TS project has better code tips when using component library.
end
Here, the source code architecture analysis of element is over. It is recommended that readers refer to the article, read the framework source code and add comments in person, so as to have a deeper understanding and facilitate the follow-up work. The next article will explain in detail the process of building a component library for the team based on element.
link
- Element source code architecture Mind map
- Element source code video, WeChat official account, reply: "Element source code video" access
- How to quickly build your own component library for the team (Part 2) -- build your own component library for the team based on element UI
- github
The article has been included in github , welcome to Watch and Star.