用web component再造UI库

358 阅读2分钟

红藕香残玉簟秋。

前情回顾

上篇文章主要大致说了web缓存 及如何配置nginx实现缓存,今天分享一个有关web-component的想法。

基础想法

customElements.define(); 方法 继承成HTMLElement ,用shadowDom将ui样式定义在组件内, ui样式用模板字符串单独定义,各个组件自由引用,整体打包为一个 js文件。

好处:

  • 项目开发只需引入一个js文件即可。
  • 同时支持多种框架
  • 样式/主题都可自定义
  • 相对于antd-design/iview/element-ui等更加轻量级。

代码示例

项目配置:react-项目

  • webpack
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    devtool:'inline-source-map',
    mode:"development",
    resolve:{
        modules:[
            path.resolve('./node_modules')
        ]
    },
    entry:{
        main:'./index.js'
    },
    output:{
        path: path.resolve(__dirname, 'build'),
        filename: 'script[name].js' 
    },
    module: {
        rules: [
            {
                test: /\.js$/, 
                exclude: /node_modules/,
                loaders:['babel-loader']
            },
            // {
            //     test: /\.css|sass|$/, 
            //     loaders:['style-loader','css-loader','sass-loader']
            // },
            
            // {
            //     test: /\.png|jpg|gif$/, 
            //     loader:'url-loader?limit=8192'
            // },
            // { test: /\.txt$/, use: 'raw-loader' }
        ]
    },
    plugins:[
        new HtmlWebpackPlugin({
            title:'',
            template:'./public/index.html',
            filename:'index.html',
            hash:true,
            cache:false
        })
    ]
}
  • babel
{
    "presets": ["@babel/preset-env","@babel/react"]
}
  • package.json
{
  "name": "noui",
  "version": "1.0.0",
  "description": "a ui frame base web component",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server  --progress --hot --host 0.0.0.0 --port 8089",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "web",
    "component",
    "ui"
  ],
  "author": "terrence",
  "license": "ISC",
  "dependencies": {
    "@babel/core": "^7.10.2",
    "@babel/preset-env": "^7.10.2",
    "@babel/preset-react": "^7.10.1",
    "babel-loader": "^8.1.0",
    "css-loader": "^3.6.0",
    "html-webpack-plugin": "^4.3.0",
    "node-sass": "^4.14.1",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-router-dom": "^5.2.0",
    "sass-loader": "^8.0.2",
    "style-loader": "^1.2.1",
    "url-loader": "^4.1.0",
    "webpack": "^4.43.0",
    "webpack-dev-server": "^3.11.0"
  },
  "devDependencies": {
    "webpack-cli": "^3.3.12"
  }
}
  • 工具函数
/**
 * Camelize a hyphen-delimited string.
 */
const camelizeRE = /-(\w)/g
export const camelize = (str) => {
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
}
/**
 * Capitalize a string.
 */
export const capitalize = (str) => {
  return str.charAt(0).toUpperCase() + str.slice(1)
}
/**
 * Hyphenate a camelCase string.
 */
const hyphenateRE = /\B([A-Z])/g
export const hyphenate =(str) => {
  return str.replace(hyphenateRE, '-$1').toLowerCase()
}
  • 项目跟页面
import React, { Component } from 'react'
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
// import { main as mainConfig } from './router/index'
// import { RenderRoutes } from './router/utils'
// import { createStore } from 'redux'
// import { Provider } from 'react-redux'
// import { allReducer } from '@/reducer/reduxs'
// import './App.css'
// import AppButton from './components/button'
// const store = createStore(allReducer)
import components from './components/index'
import {camelize,capitalize,hyphenate} from './utils/tool'
// 遍历组件 在项目中启用  这里可以单独封装
Object.keys(components).forEach(item=>{
    console.log(components[item])
    if(!customElements.get(hyphenate(item))){
        customElements.define(hyphenate(item), components[item][item]);
    }
})
class App extends Component {
  render() {
    return (
    //   <Provider store={store}>
        // <Router>
          <div className="App">
           <app-button type="primary" id="btn" toggle="true">primary</app-button>
            {/* <RenderRoutes routes={mainConfig}/> */}
          </div>
    //     </Router>
    //   </Provider>
    )
  }
}
export default App
  • button 组件
import Style from './style'
console.log(Style)
 class AppButton extends HTMLElement {
    //https://mladenplavsic.github.io/css-ripple-effect
    static get observedAttributes() { return ['disabled','icon','loading','href','htmltype'] }
    constructor() {
        super();
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.innerHTML = `
        ${Style.buttonStyle}
        <${this.href?'a':'button'} ${this.htmltype?'type="'+this.htmltype+'"':''} ${(this.download&&this.href)?'download="'+this.download+'"':''} ${this.href?'href="'+this.href+'" target="'+this.target+'" rel="'+this.rel+'"':''} class="btn" id="btn"></${this.href?'a':'button'}>
        <slot></slot>
        `
    }
    focus() {
        this.btn.focus();
    }
    get disabled() {
        return this.getAttribute('disabled')!==null;
    }
    get toggle() {
        return this.getAttribute('toggle')!==null;
    }
    get htmltype() {
        return this.getAttribute('htmltype');
    }
    get name() {
        return this.getAttribute('name');
    }
    get checked() {
        return this.getAttribute('checked')!==null;
    }
    get href() {
        return this.getAttribute('href');
    }
    get target() {
        return this.getAttribute('target')||'_blank';
    }
    get rel() {
        return this.getAttribute('rel');
    }
    get download() {
        return this.getAttribute('download');
    }
    get icon() {
        return this.getAttribute('icon');
    }
    get loading() {
        return this.getAttribute('loading')!==null;
    }
    set icon(value) {
        this.setAttribute('icon', value);
    }
    set htmltype(value) {
        this.setAttribute('htmltype', value);
    }
    set href(value) {
        this.setAttribute('href', value);
    }
    set disabled(value) {
        if(value===null||value===false){
            this.removeAttribute('disabled');
        }else{
            this.setAttribute('disabled', '');
        }
    }
    set checked(value) {
        if(value===null||value===false){
            this.removeAttribute('checked');
        }else{
            this.setAttribute('checked', '');
        }
    }
    set loading(value) {
        if(value===null||value===false){
            this.removeAttribute('loading');
        }else{
            this.setAttribute('loading', '');
        }
    }
    connectedCallback() {
        this.btn = this.shadowRoot.getElementById('btn');
        this.ico = this.shadowRoot.getElementById('icon');
        this.load = document.createElement('xy-loading');
        this.load.style.color = 'inherit';
        this.btn.addEventListener('mousedown',function(ev){
            //ev.preventDefault();
            //ev.stopPropagation();
            if(!this.disabled){
                const { left, top } = this.getBoundingClientRect();
                this.style.setProperty('--x',(ev.clientX - left)+'px');
                this.style.setProperty('--y',(ev.clientY - top)+'px');
            }
        })
        this.addEventListener('click',function(ev){
            if(this.toggle){
                this.checked=!this.checked;
            }
        })
        this.btn.addEventListener('keydown', (ev) => {
            switch (ev.keyCode) {
                case 13://Enter
                    ev.stopPropagation();
                    break;
                default:
                    break;
            }
        })
        this.disabled = this.disabled;
        this.loading = this.loading;
    }
    attributeChangedCallback (name, oldValue, newValue) {
        if(name == 'disabled' && this.btn){
            if(newValue!==null){
                this.btn.setAttribute('disabled', 'disabled');
                if(this.href){
                    this.btn.removeAttribute('href');
                }
            }else{
                this.btn.removeAttribute('disabled');
                if(this.href){
                    this.btn.href = this.href;
                }
            }
        }
        if( name == 'loading' && this.btn){
            if(newValue!==null){
                this.shadowRoot.prepend(this.load);
                this.btn.setAttribute('disabled', 'disabled');
            }else{
                this.shadowRoot.removeChild(this.load);
                this.btn.removeAttribute('disabled');
            }
        }
        if( name == 'icon' && this.ico){
            this.ico.name = newValue;
        }
        if( name == 'href' && this.btn){
            if(!this.disabled){
                this.btn.href = newValue;
            }
        }
        if( name == 'htmltype' && this.btn){
            this.btn.type = newValue;
        }
    }
}
// if(!customElements.get('xy-button')){
//     customElements.define('xy-button', AppButton);
// }
class AppButtonGroup extends HTMLElement {
    static get observedAttributes() { return ['disabled'] }
    constructor() {
        super();
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.innerHTML = `
        <style>
        :host {
            display:inline-flex;
        }
        ::slotted(xy-button:not(:first-of-type):not(:last-of-type)){
            border-radius:0;
        }
        ::slotted(xy-button){
            margin:0!important;
        }
        ::slotted(xy-button:not(:first-of-type)){
            margin-left:-1px!important;
        }
        ::slotted(xy-button[type]:not([type="dashed"]):not(:first-of-type)){
            margin-left:1px!important;
        }
        ::slotted(xy-button:first-of-type){
            border-top-right-radius: 0;
            border-bottom-right-radius: 0px;
        }
        ::slotted(xy-button:last-of-type){
            border-top-left-radius: 0;
            border-bottom-left-radius: 0;
        }
        </style>
        <slot></slot>
        `
    }
    get disabled() {
        return this.getAttribute('disabled')!==null;
    }
    set disabled(value) {
        if(value===null||value===false){
            this.removeAttribute('disabled');
        }else{
            this.setAttribute('disabled', '');
        }
    }
    connectedCallback() {
        
    }
    attributeChangedCallback (name, oldValue, newValue) {
        
    }
}
// if(!customElements.get('app-button-group')){
//     customElements.define('app-button-group', AppButtonGroup);
// }
export default {
    AppButton,
}
  • button组件样式
const buttonStyle = `<style>
                        :host{ 
                            position:relative; 
                            display:inline-flex; 
                            padding: .25em .625em;
                            box-sizing:border-box; 
                            vertical-align: middle;
                            line-height: 1.8;
                            overflow:hidden; 
                            align-items:center;
                            justify-content: center;
                            border:1px solid var(--borderColor,rgba(0,0,0,.2)); 
                            font-size: 14px; 
                            color: var(--fontColor,#333);  
                            border-radius: var(--borderRadius,.25em); 
                            transition:background .3s,box-shadow .3s,border-color .3s,color .3s;
                        }
                        :host([shape="circle"]){ 
                            border-radius:50%; 
                        }
                        /*
                        :host(:not([disabled]):active){
                            z-index:1;
                            transform:translateY(.1em);
                        }
                        */
                        :host([disabled]),:host([loading]){
                            pointer-events: none; 
                            opacity:.6; 
                        }
                        :host([block]){ 
                            display:flex; 
                        }
                        :host([disabled]:not([type])){ 
                            background:rgba(0,0,0,.1); 
                        }
                        :host([disabled]) .btn,:host([loading]) .btn{ 
                            cursor: not-allowed; 
                            pointer-events: all; 
                        }
                        :host(:not([type="primary"]):not([type="danger"]):not([disabled]):hover),
                        :host(:not([type="primary"]):not([type="danger"]):focus-within),
                        :host([type="flat"][focus]){ 
                            color:var(--themeColor,#42b983); 
                            border-color: var(--themeColor,#42b983); 
                        }
                        :host(:not([type="primary"]):not([type="danger"])) .btn::after{ 
                            background-image: radial-gradient(circle, var(--themeColor,#42b983) 10%, transparent 10.01%); 
                        }
                        :host([type="primary"]){ 
                            color: #fff; 
                            background:var(--themeBackground,var(--themeColor,#42b983));
                        }
                        :host([type="danger"]){ 
                            color: #fff; 
                            background:var(--themeBackground,var(--dangerColor,#ff7875));
                        }
                        :host([type="dashed"]){ 
                            border-style:dashed 
                        }
                        :host([type="flat"]),:host([type="primary"]),:host([type="danger"]){ 
                            border:0;
                            padding: calc( .25em + 1px ) calc( .625em + 1px );
                        }
                        :host([type="flat"]) .btn::before{ 
                            content:''; 
                            position:absolute; 
                            background:var(--themeColor,#42b983);
                            pointer-events:none; 
                            left:0; 
                            right:0; 
                            top:0; 
                            bottom:0; 
                            opacity:0; 
                            transition:.3s;
                        }
                        :host([type="flat"]:not([disabled]):hover) .btn::before{ 
                            opacity:.1 
                        }
                        :host(:not([disabled]):hover){ 
                            z-index:1 
                        }
                        :host([type="flat"]:focus-within) .btn:before,
                        :host([type="flat"][focus]) .btn:before{ 
                            opacity:.2; 
                        }
                        :host(:focus-within){ 
                            /*box-shadow: 0 0 10px rgba(0,0,0,0.1);*/ 
                        }
                        .btn{ 
                            background:none; 
                            outline:0; 
                            border:0; 
                            position: 
                            absolute; 
                            left:0; 
                            top:0;
                            width:100%;
                            height:100%;
                            padding:0;
                            user-select: none;
                            cursor: unset;
                        }
                        xy-loading{ 
                            margin-right: 0.35em;  
                        }
                        ::-moz-focus-inner{
                            border:0;
                        }
                        .btn::before{
                            content: "";
                            display: block;
                            position: absolute;
                            width: 100%;
                            height: 100%;
                            left:0;
                            top:0;
                            transition:.2s;
                            background:#fff;
                            opacity:0;
                        }
                        :host(:not([disabled]):active) .btn::before{ 
                            opacity:.2;
                        }
                        .btn::after {
                            content: "";
                            display: block;
                            position: absolute;
                            width: 100%;
                            height: 100%;
                            left: var(--x,0); 
                            top: var(--y,0);
                            pointer-events: none;
                            background-image: radial-gradient(circle, #fff 10%, transparent 10.01%);
                            background-repeat: no-repeat;
                            background-position: 50%;
                            transform: translate(-50%,-50%) scale(10);
                            opacity: 0;
                            transition: transform .3s, opacity .8s;
                        }
                        .btn:not([disabled]):active::after {
                            transform: translate(-50%,-50%) scale(0);
                            opacity: .3;
                            transition: 0s;
                        }
                        xy-icon{
                            margin-right: 0.35em;
                            transition: none;
                        }
                        :host(:empty) xy-icon{
                            margin: auto;
                        }
                        :host(:empty){
                            padding: .65em;
                        }
                        :host([type="flat"]:empty),:host([type="primary"]:empty){ 
                            padding: calc( .65em + 1px );
                        }
                        ::slotted(xy-icon){
                            transition: none;
                        }
                        :host([href]){
                            cursor:pointer;
                        }
                        </style>`
export default  {
    buttonStyle
}

思考

  • 问题一

样式作为模板字符串,抛出来以后,不利于继承,定制化;但是定制化的样式需求可以通过class 和 style 实现,似乎也并不冲突

  • 事件的透传是否需要重新自定义一套事件系统?

PS

项目地址:gitee.com/mynoe/NoUi.…

只写了几个组件,忙起来又没时间写了。。。

最后说两句

  1. 动一动您发财的小手,「点个赞吧」
  2. 动一动您发财的小手,「点个在看」
  3. 都看到这里了,不妨 「加个关注」

javascript基础知识总结