美文网首页程序员工程化
ElementUI 源码分析(一)按需加载

ElementUI 源码分析(一)按需加载

作者: 旭哥_ | 来源:发表于2020-05-18 13:33 被阅读0次

前言

本文主要给大家带来一些我在研究 ElementUI 源码过程中,自己的一些思考。

温馨提示:上个月写了一篇 Babel 相关的文章《从 Babel 到组件按需引入原理》,重点分析了 Babel 的底层原理。今天我们从 ElementUI 的实现方案出发,重点聊一聊组件库按需加载功能。希望正在研究自己创建组件库的同学可以看看,或许能对你产生一些帮助。

思考:

其实最简单的“按需加载组件”实现方式,就是在应用中直接引用所需组件的源文件,在应用的构建工具中跟应用一起构建。那么为什么不这样做呢?

在应用中构建这些组件源文件,就意味着应用的构建工具必须要具备构建这些组件的能力。比如需要有编译 Vue 模板、编译 ES6+ 语法、编译 Sass/Less 语法。甚至有些场景需要处理定制主题、国际化等等。这里会给直接在用户的应用中编译组件源码带来困难。

所以到这里我们可能想到:能不能用 webpack 实现构建多个 bundle 呢?

大致思路就是我们新增一个 webpack 配置文件,基于组件库的组件配置文件生成一个对象,key 是组件名,value 是组件的入口 js 文件,将此对象作为该配置文件的 entry 选项值即可,其他配置与完整版的组件库 webpack 配置文件一致(输出目录可根据需要自行配置)。构建时执行这两个配置文件,即可构建出一个完整版的组件库包和每个组件独立的包。

脚本生成入口文件

由于 Github 近期服务不太稳定,这里源码链接采用 Gitee 中链接代替。

了解 Element 的构建流程,我们先查看 Element 2.13.1 版本 package.jsonnpm 脚本 之一: npm run build:file

/* 自动生成一些源码文件 */
"build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js"

其中 node build/bin/build-entry.js 主要用于生成 Webpack 构建的入口文件 src/index.js

这个入口文件会对外暴露一个对象,里面会包含 install 方法。install 方法中会在传入的 vue 实例上挂载全部的组件指令全局方法。除了 install 方法外,为了支持单组件的使用, 还会在这个对外暴露对象上面添加所有的组件作为该对象的属性。

我们看源码:

/* 组件地址 */
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 OUTPUT_PATH = path.join(__dirname, '../../src/index.js');
/* 组件引入模版 */
var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\';';
/* 组件注册模版 */
var INSTALL_COMPONENT_TEMPLATE = '  {{name}}';
/* 主题字符串模版,里面有 include, install, version, list 四个变量 */
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;

/* 获取所有组件名称 */
var ComponentNames = Object.keys(Components);

/* 替换 include, install, list 的数组 */
var includeComponentTemplate = [];
var installTemplate = [];
var listTemplate = [];

ComponentNames.forEach(name => {
    /* 驼峰法命名组件,之前在 components.json 中是单词加 '-' 拼接 */
    var componentName = uppercamelcase(name);

    includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
        name: componentName,
        package: name
    }));

    /* install 方法模版排除直接挂载到全局的组件 */
    if (['Loading', 'MessageBox', 'Notification', 'Message', 'InfiniteScroll'].indexOf(componentName) === -1) {
    installTemplate.push(render(INSTALL_COMPONENT_TEMPLATE, {
        name: componentName,
        component: name
    }));
}

    /* 生成导出对象的组件名称模版 */
    if (componentName !== 'Loading') listTemplate.push(`  ${componentName}`);
});

/* 替换模版中的变量 */
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)
});

/* 将字符串模板写入到框架入口文件 */
fs.writeFileSync(OUTPUT_PATH, template);
console.log('[build entry] DONE:', OUTPUT_PATH);

为什么要编写生成框架入口文件的脚本:

  • 组件较多,减少了一些不必要的重复劳动
  • 实现构建框架的流程配置自动化

仔细想一下,我们开发公共组件库的目的,就是让公司所有前端团队做项目更加便利。那么,在写脚本提升我们开发效率的方面,也是值得我们深入探索的。之前有看过一篇掘友文章,充分利用 NodeJS动态创建 vue 组件,配置路由的。感兴趣的小伙伴可以试一试。

脚本做了哪些事情:

  • 引入插件
  • 初始化字符串模版
  • 组织数据,用于替换字符串模版中变量
  • 替换模版中变量,并将模板写入到框架入口文件

入口文件内容

看下脚本执行的结果 element/src/index.js

/* Automatically generated by './build/bin/build-entry.js' */

/* 组件源码引入 */
import Pagination from '../packages/pagination/index.js';
import Dialog from '../packages/dialog/index.js';
import Autocomplete from '../packages/autocomplete/index.js';
import Dropdown from '../packages/dropdown/index.js';
import DropdownMenu from '../packages/dropdown-menu/index.js';
// ...
import Avatar from '../packages/avatar/index.js';
import Drawer from '../packages/drawer/index.js';
import Popconfirm from '../packages/popconfirm/index.js';
import locale from 'element-ui/src/locale';
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';

const components = [
    Pagination,
    Dialog,
    Autocomplete,
    Dropdown,
    DropdownMenu,
    // ...
    CascaderPanel,
    Avatar,
    Drawer,
    Popconfirm,
    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: '2.13.1',
    locale: locale.use,
    i18n: locale.i18n,
    install,
    CollapseTransition,
    Loading,
    Pagination,
    Dialog,
    Autocomplete,
    Dropdown,
    DropdownMenu,
    // ...
    PageHeader,
    CascaderPanel,
    Avatar,
    Drawer,
    Popconfirm
};

入口文件干了什么:

  • 导入所有组件
  • 定义 install 方法
  • 判断环境执行 install 方法
  • 最后整体导出

组件打包

webpack --config build/webpack.component.js 脚本用于构建 commonjs2 的UI组件(提供按需引入功能),执行该脚本最终会在 lib 下生成所有 Element 支持的 UI 组件如图目录(同时这些文件也会被 element-ui.common.js 总入口文件引用)。

image

不废话,直接看源码 webpack.component.js

const path = require('path');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');

/* 所有组件的构建入口列表 */
const Components = require('../components.json');
const config = require('./config');

const webpackConfig = {
    mode: 'production',
    /* 多入口 */
    entry: Components,
    output: {
        path: path.resolve(process.cwd(), './lib'),
        publicPath: '/dist/',
        filename: '[name].js',
        chunkFilename: '[id].js',
        libraryTarget: 'commonjs2'
    },
    resolve: {
        extensions: ['.js', '.vue', '.json'],
        alias: config.alias,
        modules: ['node_modules']
    },
    /* 排除其他UI组件、支持国际化、utils的源码,这些源码会额外构建 */
    externals: config.externals,
    performance: {
        hints: false
    },
    stats: 'none',
    optimization: {
        minimize: false
    },
    module: {
        rules: [
            {
                test: /\.(jsx?|babel|es6)$/,
                include: process.cwd(),
                exclude: config.jsexclude,
                loader: 'babel-loader'
            },
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                options: {
                    compilerOptions: {
                        preserveWhitespace: false
                    }
                }
            },
            {
                test: /\.css$/,
                loaders: ['style-loader', 'css-loader']
            },
            {
                test: /\.(svg|otf|ttf|woff2?|eot|gif|png|jpe?g)(\?\S*)?$/,
                loader: 'url-loader',
                query: {
                    limit: 10000,
                    name: path.posix.join('static', '[name].[hash:7].[ext]')
                }
            }
        ]
    },
    plugins: [
        new ProgressBarPlugin(),
        new VueLoaderPlugin()
    ]
};

module.exports = webpackConfig;

构建单个组件和构建总体入口文件 element-ui.common.jsWebpack 配置类似,需要将 utilslocale 以及其他一些依赖排除。

文章写到这里就结束了,其实还有很多细节值得去研究,比如:样式的处理国际化的实现等等。那么大家感兴趣的就直接去 ElementUI 源码寻找答案吧!😅

参考文章

感谢

如果本文对你有帮助,就点个赞支持下吧!感谢阅读。

相关文章

网友评论

    本文标题:ElementUI 源码分析(一)按需加载

    本文链接:https://www.haomeiwen.com/subject/kleuohtx.html