搭建一个基于react+TS+antd的组件库(2)

项目github地址javascript

webpack篇

上一篇主要讲述了npm package的发布、更新、删除、开发过程当中的调试,以及拥有一个私有库的几种方式,这篇来说讲怎么把咱们写的代码编译打包(即各类语法转换成ES5)出来后,各个环境(浏览器、node)均可以使用,且不局限引用方式,便可以用ES6的import,node的require,以及script标签。咱们先从babel入手。css

babel

babel is a JavaScript compilerhtml

Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments. Here are the main things Babel can do for you:前端

  • Transform syntax
  • Polyfill features that are missing in your target environment (through @babel/polyfill)
  • Source code transformations (codemods)
  • And more! (check out these videos for inspiration)

——摘抄 babeljava

babel入门

The entire process to set this up involves:node

  1. Running these commands to install the packages:
npm install --save-dev @babel/core @babel/cli @babel/preset-env
npm install --save @babel/polyfill复制代码

   2. Creating a config file named babel.config.json in the root of your project with this content:react

{
  "presets": [
    [
      "@babel/env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1",
        },
        "useBuiltIns": "usage",
      }
    ]
  ]
}复制代码

3. And running this command to compile all your code from the src directory to lib:jquery

./node_modules/.bin/babel src --out-dir lib复制代码

You can use the npm package runner that comes with npm@5.2.0 to shorten that command by replacing ./node_modules/.bin/babel with npx babelwebpack

——摘抄 babel 指南-Usage Guidegit


进入正题,怎么一步一步把ES6+react+TS编译成浏览器认识的代码呢?

【提问:我想要在组件库中使用ES6/7/8/9等等最新的javascript语法,但是浏览器不兼容怎么办?】

@babel/preset-env

@babel/preset-env is a smart preset that allows you to use the latest JavaScript without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s). This both makes your life easier and JavaScript bundles smaller!

——摘抄 babel presets-env

npm install --save-dev @babel/preset-env

yarn add @babel/preset-env --dev复制代码


【提问:个人组件库是用react写的,react又要怎么转换呢?】

@babel/preset-react

This preset always includes the following plugins:

And with the development option:

——摘抄 babel presets-react

npm install --save-dev @babel/preset-react

yarn add @babel/preset-react --dev复制代码


【小白提问:我打算用TypeScript来写个人组件库,避免我编程的时候犯的一些低级错误,对组件使用者也相对更友好一些,那ts又须要用什么转换呢?】

@babel/preset-typescript

This preset includes the following plugins:

You will need to specify --extensions ".ts" for @babel/cli & @babel/node cli's to handle .ts files.

npm install --save-dev @babel/preset-typescript复制代码

——摘抄 babel presets-typescript

Usage

// presets逆序执行(从后往前)ts -> react -> ES6/7 
// preset的参数怎么写,有哪些,请自行查阅官方文档,这里不展开
{
  "presets": ["@babel/preset-env","@babel/preset-react","@babel/preset-typescript"]
}复制代码


补充

@babel/polyfill

Now luckily for us, we're using the env preset which has a "useBuiltIns" option that when set to "usage" will practically apply the last optimization mentioned above where you only include the polyfills you need. With this new option the configuration changes like this:

{
  "presets": [
    [
      "@babel/preset-env",
      {
            "useBuiltIns": "usage", // https://www.babeljs.cn/docs/usage#polyfill
      }
    ],
    "@babel/preset-react",
    "@babel/preset-typescript"
  ]
}复制代码

【笔者理解】简单的来讲,useBuiltIns设置为usage,babel会自动import对应的modules,简单方便。参考

// In a.js
var a = new Promise();

// Out (if environment doesn't support it) import "core-js/modules/es.promise"; var a = new Promise(); // Out (if environment supports it) var a = new Promise();复制代码


@babel/plugin-proposal-decorators

编译装饰器

Simple class decorator

@annotation
class MyClass { }

function annotation(target) {
   target.annotated = true;
}复制代码

若是legacy字段设为true的话,就要配合@babel/plugin-proposal-class-properties使用,且loose要设置为true参考

{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose" : true }]
  ]
}复制代码


@babel/plugin-transform-runtime

A plugin that enables the re-use of Babel's injected helper code to save on codesize.

Instance methods such as "foobar".includes("foo") will only work with core-js@3. If you need to polyfill them, you can directly import "core-js" or use @babel/preset-env's useBuiltIns option.

The plugin transforms the following:

var sym = Symbol();

var promise = Promise.resolve();

var check = arr.includes("yeah!");

console.log(arr[Symbol.iterator]());复制代码

into the following:

import _getIterator from "@babel/runtime-corejs3/core-js/get-iterator";
import _includesInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/includes";
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";
import _Symbol from "@babel/runtime-corejs3/core-js-stable/symbol";

var sym = _Symbol();

var promise = _Promise.resolve();

var check = _includesInstanceProperty(arr).call(arr, "yeah!");

console.log(_getIterator(arr));复制代码

——摘抄 babel 用法-transform-runtime


【笔者理解】能够自动引入对应Babel's injected helper code,同use @babel/preset-env's useBuiltIns option


总结

基础配置ok了,其余的语法须要babel解析的话,能够再自行查找babel-plugins

(说一下笔者的操做,先一顿狂写,而后编译一下,babel会报错,报啥错,就安装啥插件,简单粗暴。每次的错误都要用心记录下来哦,这样之后就能够提早安装好须要的各类babel plugins了)

模块概念

ES6 Module的加载实现(比较了ES6和CommonJs的差别、循环加载等)

CommonJS

  • 全部代码都运行在模块做用域,不会污染全局做用域;
  • 模块是同步加载的,即只有加载完成,才能执行后面的操做;
  • 模块在首次执行后就会缓存再次加载只返回缓存结果,若是想要再次执行,可清除缓存;
  • CommonJS输出是值的拷贝(即,require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值)。


基本用法

//a.js
module.exports = function () {
  console.log("hello world")
}

//b.js
var a = require('./a');

a();//"hello world"

//或者

//a2.js
exports.num = 1;
exports.obj = {xx: 2};

//b2.js
var a2 = require('./a2');

console.log(a2);//{ num: 1, obj: { xx: 2 } }
复制代码

——摘抄 掘金 再次梳理AMD、CMD、CommonJS、ES6 Module的区别


AMD和require.js

异步加载,依赖前置,提早执行

//a.js
//define能够传入三个参数,分别是字符串-模块名、数组-依赖模块、函数-回调函数
define(function(){
    return 1;
})

// b.js
//数组中声明须要加载的模块,能够是模块名、js文件路径
require(['a'], function(a){
    console.log(a);// 1
});复制代码


CMD和sea.js

异步加载,依赖就近,延迟执行

/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
     // 等于在最前面声明并初始化了要用到的全部模块
    a.doSomething();
    if (false) {
        // 即使没用到某个模块 b,但 b 仍是提早执行了
        b.doSomething()
    } 
});

/** CMD写法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在须要时申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});
复制代码

——摘抄 掘金 前端模块化:CommonJS,AMD,CMD,ES6


ES6

  • CommonJS模块是运行时加载,ES6 Module是编译时输出接口
  • CommonJS加载的是整个模块,将全部的接口所有加载进来,ES6 Module能够单独加载其中的某个接口
  • CommonJS输出是值的拷贝,ES6 Module输出的是值的引用,被输出模块的内部的改变会影响引用的改变;
  • CommonJS this指向当前模块,ES6 Module this指向undefined;


——摘抄 掘金 再次梳理AMD、CMD、CommonJS、ES6 Module的区别

// a.js
const function a() => {
    console.log("this is in a");
}
export {
    a,
}

// b.js
import { a } from "./a";
a(); // this is in a
复制代码


webpack模块编译配置项

webpack 概念-modules

webpack 指南-建立library

webpack 配置-output.libraryTarget

webpack 配置-output.library


咱们但愿包能够在任何的环境下运行,支持常见的三种引用方式

  • import引用
  • require引用
  • script标签引用

因此输出的libraryTarget要配置为umd

libraryTarget: "umd" - 将你的 library 暴露为全部的模块定义下均可运行的方式。它将在 CommonJS, AMD 环境下运行,或将模块导出到 global 下的变量。了解更多请查看 UMD 仓库

webapck.config.json

var path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js',
    library: "MyLibrary",
    libraryTarget: "umd"
  }
};复制代码

最终输出

(function webpackUniversalModuleDefinition(root, factory) {
  if(typeof exports === 'object' && typeof module === 'object')
    module.exports = factory();
  else if(typeof define === 'function' && define.amd)
    define([], factory);
  else if(typeof exports === 'object')
    exports["MyLibrary"] = factory();
  else
    root["MyLibrary"] = factory();
})(typeof self !== 'undefined' ? self : this, function() {
  return _entry_return_; // 此模块返回值,是入口 chunk 返回的值
});复制代码

——摘自 webpack 配置-output.libraryTarget-模块定义系统-umd


扩展

  • node中Module又是怎么一回事?


webpack配置

webpack 配置-libraryTargets【这些选项将致使 bundle 带有更完整的模块头部,以确保与各类模块系统的兼容性。根据 output.libraryTarget 选项不一样,output.library 选项将具备不一样的含义。】

webpack 指南-建立library

webpack 配置-externals防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些

扩展依赖(external dependencies)
。】

webpack 配置-targets【webpack能够编译成不一样环境下运行的代码,例如node、web(默认)】

webpack 指南-构建性能


安装如下依赖,配置一个最基础的webpack

npm install webpack webpack-cli -D复制代码

webpack.condig.js

const path = require('path');

module.exports = {
  mode:'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    library: "MyLibrary",
    libraryTarget: "umd",
    publicPath: "./",
  }
};复制代码

loader

  • loader 用于对模块的源代码进行转换
  • 逆向执行

plugins

  • plugins目的在于解决 loader 没法实现的其余事
  • 正常顺序执行


ES6

安装如下依赖

npm install @babel/core @babel/preset-env @babel/plugin-transform-runtime -D
npm install babel-loader -D 复制代码

webpack编译ES6的配置以下:

// ./webapck.config.js
var path = require('path');

module.exports = {
  mode: process.env.NODE_ENV,
  entry: { index: './src/index.js' },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    library: "MyLibrary",
    libraryTarget: "umd"
  },
  externals:
    !process.env.debug
      ? ["react", "react-dom"]
      : {
          React: "react",
          ReactDOM: "react-dom"
        },
  module: {
    rules: [
    {
        test: /\.(jsx|js)$/,
         use: [
            {
            loader: "babel-loader",
            options: {
                presets: ["@babel/env"],
                plugins: ["@babel/plugin-transform-runtime"]
            }
          },
        ],
        include: [path.resolve(__dirname, "src")],
        exclude: /(node_modules|bower_components)/,
      }
    ]
  }
};复制代码


typescript

webpack 指南-TypeScript

ts-loader

tsconfig.json配置说明(官方)

tsconfig.json配置文件(官方)

tsconfig.json配置详解


安装如下依赖

npm install typescript
npm install ts-loader -D复制代码

webpack编译TS的配置以下,具体分析见下一篇

module: {
    rules: [
    {
        test: /\.(tsx|ts)$/,
         use: [
            {
            loader: "babel-loader",
            options: {
                presets: ["@babel/env"],
                plugins: ["@babel/plugin-transform-runtime"]
            }
          },
         + { loader: "ts-loader" }
        ],
        include: [path.resolve(__dirname, "src")],
        exclude: /(node_modules|bower_components)/,
      }
    ]
  },复制代码
// tsconfig.json
{
  "compilerOptions": {
    "declaration": true, // 生成相应的 .d.ts文件。
    "declarationDir": "./types", // 生成声明文件的输出路径。
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    },
    "allowSyntheticDefaultImports": true, // 容许从没有设置默认导出的模块中默认导入。这并不影响代码的输出,仅为了类型检查。
    "experimentalDecorators": true, // 启用实验性的ES装饰器。
    "module": "ES6",
    "target": "ES6",
    "skipLibCheck": true, // 忽略全部的声明文件( *.d.ts)的类型检查。
    "esModuleInterop": true, // 经过导入内容建立命名空间,实现CommonJS和ES模块之间的互操做性
    "moduleResolution": "node", // 决定如何处理模块。或者是"Node"对于Node.js/io.js,或者是"Classic"(默认)。
    "strict": true, // 启用全部严格类型检查选项。
    "removeComments": false, // 删除全部注释,除了以 /!*开头的版权信息。
    "jsx": "react", // 在 .tsx文件里支持JSX: "React""Preserve""sourceMap": true, // 生成相应的 .map文件。
    "downlevelIteration": true // 当target为"ES5""ES3"时,为"for-of" "spread""destructuring"中的迭代器提供彻底支持
  },
  "exclude": ["node_modules", "build", "scripts", "**/*.css"] // 表示要排除的,不编译的文件
}

复制代码

react

安装如下依赖

npm install @types/react @types/react-dom 
npm install @babel/preset-react -D复制代码

webpack编译react的配置以下:

options: {
    - presets: ["@babel/preset-env"],
    + presets: ["@babel/preset-env", "@babel/preset-react"], 
      plugins: ["@babel/plugin-transform-runtime"]
}复制代码

要使用装饰器的语法的话,须要安装

npm install @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties -D复制代码

plugins: [
  // https://babeljs.io/docs/en/babel-plugin-proposal-decorators  // If you are including your plugins manually and using @babel/plugin-proposal-class-properties, make sure that @babel/plugin-proposal-decorators comes before @babel/plugin-proposal-class-properties.  // When using the legacy: true mode, @babel/plugin-proposal-class-properties must be used in loose mode to support the @babel/plugin-proposal-decorators.  [    "@babel/plugin-proposal-decorators",     {     // Use the legacy (stage 1) decorators syntax and behavior.       legacy: true     }  ],  ["@babel/plugin-proposal-class-properties", { loose: true }],
  "@babel/plugin-transform-runtime"]复制代码

@babel/plugin-proposal-decoratorslegacy设为 true的话须要配置 @babel/plugin-proposal-class-propertiesloosetrue详见文档

antd

安装如下依赖

npm install antd 
npm install babel-plugin-import -D复制代码

按需加载,参考babel-plugin-import

options: {
    presets: ["@babel/env", "@babel/react"],
      plugins: [
       + [
       +  "import",
       +  {
       +    libraryName: "antd",
       +    // libraryDirectory: "es", // 默认lib
       +    style: true // `style: true` 会加载 less 文件
       +  }
       +],
        ["@babel/plugin-proposal-decorators", { legacy: true }],
        ["@babel/plugin-proposal-class-properties", { loose: true }],
        "@babel/plugin-transform-runtime"
      ]
  }复制代码


less

对less文件作如下处理


安装如下依赖

npm install less-loader css-loader style-loader -D复制代码
{
        test: /\.less$/,
        use: [
          {
            loader:"style-loader"
          },
          {
            loader: "css-loader",
            options: {
              modules: {
                // localIdentName: '[path][name]__[local]',
                getLocalIdent: (context, _, localName) => {
                  if (context.resourcePath.includes("node_modules")) {
                    return localName;
                  }
                  return `demo__${localName}`;
                },
              },
            },
          }, 
          {
            loader: "less-loader",
            options: {
              lessOptions: {
                // http://lesscss.org/usage/#command-line-usage-options
                javascriptEnabled: true,
                modifyVars: {
                  // "primary-color": "#1DA57A",
                  // "link-color": "#1DA57A",
                  // "border-radius-base": "2px",
                  // or
                  // https://github.com/ant-design/ant-design/blob/d2214589391c8fc8646c3e8ef2c6aa86fcdd02a3/.antd-tools.config.js#L94
                  hack: `true; @import "${require.resolve( "./src/assets/style/ui.config.less" )}";` // Override with less file
                }
              }
            }
          }
        ]
      },复制代码

You can pass any Less specific options to the less-loader via loader options. See the Less documentation for all available options in dash-case.

——摘自 webpack less-loader


解释:globalVar&modifyVars

Global Variables

命令行写法

json配置写法

lessc --global-var="color1=red" { globalVars: { color1: 'red' } }

This option defines a variable that can be referenced by the file. Effectively the declaration is put at the top of your base Less file, meaning it can be used but it also can be overridden if this variable is defined in the file.

Modify Variables

命令行写法

json配置
lessc --modify-var="color1=red" { modifyVars: { color1: 'red' } }

As opposed to the global variable option, this puts the declaration at the end of your base file, meaning it will override anything defined in your Less file.

——摘抄 less官方文档


css module

css module文档


参考资料

相关文章
相关标签/搜索