从零学脚手架(五)---react、browserslist

2021-03-16 15:29:14 浏览数 (1)

如果此篇对您有所帮助,在此求一个star。项目地址: OrcasTeam/my-cli

react

react介绍

目前,国内主流的前端应用框架具有两个:vue.js和react.js,关于vue和react的优劣性,网上众说纷纭。在下就不在此引战。

而是直接介绍React

??? vue和React这种都是快速应用开发工具,可能也会像曾经如日中天的JQuery被市场淘汰,所以个人建议不要盲目只追求快速工具的使用,而是花时间去学习原点。例如设计思想数据结构。快速应用框架(或语言)只不过是应用工具而已。

? 以前都说是“三大框架”,还有一个Google开发的Angular,但是国内Angular使用份额越来越少。 个人感觉Angular主要问题是上手成本。Angular比较偏向于后端,很多概念对于前端开发人员都是噩梦。不过对于前端工程化,个人认为Angular是集大成之作。个人建议,对于有经验的朋友,可以稍微学习下Angular中的思想。

React是一个用于构建用户界面的 JavaScript 库,

React本身是一个特别简单的库:将元素抽象为虚拟DOM,更新DOM时对比虚拟DOM,然后只更新那些真正需要更新的元素。

React.createElement()

使用Document构建DOM时,都是使用 document.createElement() 来构建标签

代码语言:javascript复制
const li =  document.createElement('li');
document.body.appendChild(li)

在React中, 也提供了这样一个自定义函数来React组件。

React.createElement() 返回的是一个React自定义的元素类型:ReactElement

代码语言:javascript复制
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

React提供的React.createElement()ReactElement提供了很好平台隔离性。

使用同一套代码编写的元素组件只需要对接不同平台的APi,就可以实现跨平台。

React能够跨平台的原因也在于此。

在日常开发中,也会经常写无关业务的通用封装,其思想与此类似。

虚拟DOM

在直接使用Document更新DOM元素时,很多时候会因为某些原因 对不必更新DOM进行更新 从而产生了性能浪费

解决这个问题一般想到的做法就是做一个DOM缓存。创建DOM时将DOM信息缓存,更新时对比新旧DOM。排除掉不必要的更新DOM。

这种缓存DOM数据的方案就叫虚拟DOM(Virtual DOM), 而排除算法叫做diff算法

React也使用了这种方案提升性能

虚拟DOM(Virtual DOM)和diff算法 是对数据结构和算法的考验。每一个人都可以模拟出简单的方案,但不是每一个人都可以写出优秀的解决方案。

在下愚钝,对于数据结构和算法掌握的不好。所以对虚拟DOM(Virtual DOM)和diff算法只有浅薄的认知。有兴趣的朋友可以看一下这篇文章:深度剖析:如何实现一个 Virtual DOM 算法

JSX

React是通过JS构建元素的,

我们都知道使用JS编写页面痛苦是没有结构性。

使用HTML两个标签能搞定的事,使用JS就能写一大堆代码。

React为了解决这个问题,提供了一个模板语言---JSX

JSX是一种JS扩展语言。允许在JS中以标签形式构建元素。并且JSX开发工具中还可以具有各种提示和快捷键。

能够极大的提高开发效率

代码语言:javascript复制
const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

但JSX编写的组件只是React.createElement()语法糖,打包编译过程中会将JSX语法转换为React.createElement()

??? JSX编写的组件本质是 React.createElement() 语法糖。所以React还支持使用 React.createElement() 创建虚拟DOM(Virtual DOM)。

?? JSX是React提供构建代码方式的一种扩展语言,本质是一个语法糖。JSX定义的事件styleclass是JSX自身语法,并不是原生DOM。所以有些属性名称不一致。

?? JSX转换React.createElement()操作使用的是babel提供的一个plugin,在下面再介绍

? JSX目前被社区认可。Vue@3.X也支持JSX

添加 React
安装 react

React目前最新版本为17.0.1,在这里就直接引用此版本来介绍,对React有兴趣的朋友在从老版本循循渐进的学习。

yarn add react@17.0.1

react库是React的核心库,具有 React.createElement()虚拟DOMJSX语法支持等一系列核心内容。

但是此库并不没有提供与真实DOM交互。与真实DOM交互的代码则由react-dom提供,

yarn add react-dom@17.0.1

react类似一个通用库,没有与任何平台具有相关性,只负责组织数据结构。

就像写React Native时,使用了react-native来做平台交互。

使用 react

接下来就仿照react-cli来组织代码。

根节点

第一步就是在HTML页面中创建一个元素作为React承载的根节点。

? vue-cli也具有这么一个根节点用来承载vue,只不过元素ID名称不一样,有兴趣的朋友可以自行查看。

接下来处理JS,在之前打包测试中都是使用 /src/index.js 文件作为源文件

也是使用此文件作为源文件

?? React只是承载在打包器中的一个应用框架。经过打包器打包将JSX转换为可运行的代码。

代码语言:javascript复制
import React from 'react';
import ReactDOM from 'react-dom';

const root = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);
ReactDOM.render(root, document.getElementById('root'));

/src/index.js 文件中使用了JSX创建元素,然后使用

react-dom中的 ReactDOM.render() 添加到根节点中。

? vue-cli也同样如此,有兴趣的朋友可以自行查看

@babel/preset-react

不过如果此时执行yarn build操作,会直接报错。

这是因为JSX无法被识别的问题。前面说过,JSX只是React提供的一种模板语言。本质上并不属于JS模块。

所以需要将JSX转换为 React.createElement() 形式

提供这个转换操作的是babel中提供的一个plugin:@Babel/plugin-syntax-jsx

不过不需要直接安装这个plugin

babel为React提供了一个preset:@babel/preset-react。

@babel/preset-react中封装了所有处理React的plugin

yarn add -D @babel/preset-react@7.12.13

? Babel官网提供了JSX转换为 React.createElement() 的测试,有兴趣的朋友可以测试测试

然后配置在 .babelrc 文件中

此时执行yarn build便可以执行成功,并且查看生成代码可以看到JSX已经转换为了React.createElement()

在浏览器也可以正常运行代码

.jsx文件
app.jsx

React代码已经运行成功,接下来就组织React代码。

刚才,直接在 /src/index.js 文件中编写了JSX代码进行测试

但是真正开发中,需要将JSX代码编写在 .jsx 文件中,通过模块导入导入方式提供给 /src/index.js 文件。

将JSX提取到 /src/app.jsx 文件,在 /src/index.js 导入。

?? app.jsx作为React框架的根节点。用在承载React组件。

/src/app.jsx 文件中组件作为React的根节点。React也是以树的组织方式管理,/src/app.jsx 文件中组件就是树根。React框架代码就像 托管 在了 /src/app.jsx 之中

? ?

  • React组件分为 函数组件类组件函数组件 方便,再加上 Hooks 的助力,在编写颗粒度较小组件时使用 函数组件 是个非常好的选择。类组件 封装性强,内部提供完善的钩子函数和一系列功能,再加上继承特性。比较适合使用在业务代码主干中。
  • /src/app.jsx 中返回的 <></> 代表 空标签 ,React组件只允许返回一个元素,但有时候组件需要返回元素数组,可以在外部包一层空标签。与Vue中的template标签功能一致。
  • React 组件名称约定为大写形式
webpack配置

.jsx作为一种新的文件格式,需要在webpack进行配置使用babel

代码语言:javascript复制
const modules = {
  module:{
    rules:[
      {
        //  所有的.js或者.jsx文件都走babel-loader
        test: /.js(x?)$/,
        include: path.join(config.root,'src'),
        loader: "babel-loader"
      }
    ]
  }
}

并且可以提供引用时忽略后缀名称。

代码语言:javascript复制
 resolve:{
    //  可被忽略的后缀
    extensions:['.jsx', '.js', '.json'],
  }

此时就算成功将React使用在脚手架中了。

而对于React Router、Redux只是用于扩展React的开发库。在此就不再添加。

? vue-cli搭建方式与react-cli基本一致,只是各自框架暴露的API不同

browserslist

browserslist是什么

在介绍babel时使用过package.json文件中browserslist属性设置浏览器版本,那么browserslist属性到底是怎么回事呢?

前面介绍过,前端的运行环境(浏览器)版本是由用户决定的,不同的项目对于浏览器版本要求不一样。

而在打包过程中。需要指定支持的浏览器版本,以这些版本对开发代码做出适配。(CSS、JS都需要适配)。

browserslist属性就是提供指定浏览器版本功能。是由browserslist库提供的。

而这个简单的功能browserslist却做出了强大的效果,得到了社区的高度认可。很多库都直接依赖browserslist

browserslist配置方式

browserslist提供了两种配置方式。

一种就是配置在package.json文件中的browserslist属性。browserslist执行时会默认读取此属性。

另一种是使用约定文件。可以在项目根目录(package.json所在目录)创建一个约定文件 .browserslistrc.json ,将属性配置在此。.browserslistrc.json文件名称一般会省略后缀:.browserslistrc

两种方式不可同时设置,否则会直接报错。

个人推荐直接配置在package.json文件中,没必要创建一个文件了。在此也就直接使用此方案。

browserslist环境变量

browserslist可以使用不用属性来灵活的控制浏览器版本。

如下所示。可以设置在不同环境下设置不同浏览器版本。

代码语言:javascript复制
"browserslist": {
    "development": [
        "chrome > 75"
    ],
     "production": [
         "ie 9"
     ]
}

属性值取自Node.js中环境变量。环境变量名称为BROWSERSLIST_ENV。所以需要设置环境变量。

注意:在此虽然设置在webpack.config.js文件中,但设置的是Node.js中的环境变量, 并不是webpack提供的环境变量。

browserslist属性值名称可以随意命名。只要与Node.js中BROWSERSLIST_ENV环境变量对应即可。

在此就不贴图测试了,有兴趣的朋友可以自行测试。

至于BROWSERSLIST_ENV 环境变量与 webpack中不同模式的关联,在下一篇介绍。

browserslist支持的浏览器

browserslist支持设置当前基本上所有的浏览器,在Github上作者说明了可以设置的浏览器

可以看到,browserslist几乎支持所有浏览器:PC、安卓、IOS 甚至还有国内浏览器。

?? 设置浏览器时名称不区分大小写

browserslist属性

browserslist能得到社区的认可,也就在于browserslist提供了强大的属性设置。

如前面使用的 指定 区间浏览器(chrome > 75) 也只是browserslist简单的属性配置

下面简单列举部分browserslist属性配置,想了解更多的朋友请参考Github

一般只需要简单的设置即可。

总结

???

  • React是一个快速构建高性能网站的开发框架
  • React使用了虚拟DOM(Virtual DOM)和diff 算法优化了DOM操作
  • React利用自定义DOM类型解耦平台限制,以此实现了跨平台
  • JSX只是一个JS扩展语法。React使用JSX作为构建元素的模板语言
  • browserslist是一个强大的设置浏览器版本库。

本文参考

  • vue核心之虚拟DOM(vdom)
  • 深度剖析:如何实现一个 Virtual DOM 算法
  • browserslist Github
  • babel-preset-react

本文依赖

  • react@17.0.1
  • react-dom@17.0.1
  • @babel/preset-react@7.12.3

package.json

代码语言:javascript复制
{
  "name": "my-cli",
  "version": "1.0.0",
  "main": "index.js",
  "author": "mowenjinzhao<yanzhangshuai@126.com>",
  "license": "MIT",
  "devDependencies": {
    "@babel/core": "7.13.1",
    "@babel/plugin-transform-runtime": "7.13.7",
    "@babel/preset-env": "7.13.5",
    "@babel/preset-react": "7.12.13",
    "@babel/runtime-corejs3": "7.13.7",
    "babel-loader": "8.2.2",
    "clean-webpack-plugin": "3.0.0",
    "html-webpack-plugin": "5.2.0",
    "webpack": "5.24.0",
    "webpack-cli": "4.5.0"
  },
  "dependencies": {
    "jquery": "3.5.1",
    "react": "17.0.1",
    "react-dom": "17.0.1"
  },
  "scripts": {
    "start": "webpack --mode=development  --config webpack.config.js",
    "build": "webpack --mode=production  --config webpack.config.js"
  },
  
  "browserslist": [
    "ie 9",
    "Chrome > 75"
    ]
}

webpack.config.js

代码语言:javascript复制
const path = require('path')
const webpack = require("webpack");
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')

//	browserslist环境变量
process.env.BROWSERSLIST_ENV = 'development'

const config = {
  root: path.join(__dirname, './'),
}

const modules = {

  //  入口文件
  //  字符串形式
  entry: path.join(config.root, 'src/index.js'),
  //  对象形式
  // entry:{
  //   'index':  path.join(config.root, 'src/index.js'),
  // },

  //  输出文件
  //  字符串形式
  // output:path.join(config.root, './dist/[name].js')
  //对象形式
  output: {
    //  输出文件的目录地址
    path: path.join(config.root, 'dist'),
    //  输出文件名称,contenthash代表一种缓存,只有文件更改才会更新hash值,重新打包
    filename: '[name]_[contenthash].js'
  },

  //devtool:false, //'eval'

  module:{
    rules:[
      {
        //  所有的.js(x?)文件都走babel-loader
        test: /.js(x?)$/,
        include: path.join(config.root,'src'),
        loader: "babel-loader"
      }
    ]
  },


  optimization: {
    minimize: false,
    minimizer: [
    new TerserPlugin({
          //  指定压缩的文件
          include: /.js(?.*)?$/i,

          // 排除压缩的文件
          // exclude:/.js(?.*)?$/i,

          //  是否启用多线程运行,默认为true,开启,默认并发数量为os.cpus()-1
          //  可以设置为false(不使用多线程)或者数值(并发数量)
          parallel: true,

          //  可以设置一个function,使用其它压缩插件覆盖默认的压缩插件,默认为undefined,
          minify: undefined,

          //  是否将代码注释提取到一个单独的文件。
          //  属性值:Boolean | String | RegExp | Function<(node, comment) -> Boolean|Object> | Object
          //  默认为true, 只提取/^**!|@preserve|@license|@cc_on/i注释
          //  感觉没什么特殊情况直接设置为false即可
          extractComments: false,

          // 压缩时的选项设置
          terserOptions: {
            //  是否保留原始函数名称,true代表保留,false即保留
            //  此属性对使用Function.prototype.name
            //  默认为false
            keep_fnames: false,

            // 是否保留原始类名称
            keep_classnames: false,

            //  format和output是同一个属性值,,名称不一致,output不建议使用了,被放弃
            // 指定压缩格式。例如是否保留*注释*,是否始终为*if*、*for*等设置大括号。
            format: {
              comments: false,
            },
            output: undefined,

            //  是否支持IE8,默认不支持
            ie8: false,

            compress: {
              // 是否使用默认配置项,这个属性当只启用指定某些选项时可以设置为false
              defaults: false,

              // 是否移除无法访问的代码
              dead_code: false,

              // 是否优化只使用一次的变量
              collapse_vars: true,

              warnings: true,

              //  是否删除所有 console.*语句,默认为false,这个可以在线上设置为true
              drop_console: false,

              //  是否删除所有debugger语句,默认为true
              drop_debugger: true,

              //  移除指定func,这个属性假定函数没有任何副作用,可以使用此属性移除所有指定func
              // pure_funcs: ['console.log'], //移除console
            },
          },
    	})
    ]
  },

  plugins: [
    new HtmlWebpackPlugin({
       //  HTML的标题,
        //  template的title优先级大于当前数据
        title: 'my-cli',

        //  输出的html文件名称
        filename: 'index.html',

        //  本地HTML模板文件地址
        template: path.join(config.root, 'src/index.html'),

        // 引用JS文件的目录路径
        publicPath: './',

        //  引用JS文件的位置
        //  true或者body将打包后的js脚本放入body元素下,head则将脚本放到中
        //  默认为true
        inject: 'body',

        //  加载js方式,值为defer/blocking
        //  默认为blocking, 如果设置了defer,则在js引用标签上加上此属性,进行异步加载
        scriptLoading: 'blocking',

        //  是否进行缓存,默认为true,在开发环境可以设置成false
        cache: false,

        //  添加mate属性
        meta: {}
    }),

    new CleanWebpackPlugin({
 		// 是否假装删除文件
        //  如果为false则代表真实删除,如果为true,则代表不删除
        dry: false,

        //  是否将删除日志打印到控制台 默认为false
        verbose: true,

        //  允许保留本次打包的文件
        //  true为允许,false为不允许,保留本次打包结果,也就是会删除本次打包的文件
        //  默认为true
        protectWebpackAssets: true,

        //  每次打包之前删除匹配的文件
        cleanOnceBeforeBuildPatterns: ['**/*'],

        //  每次打包之后删除匹配的文件
        cleanAfterEveryBuildPatterns:["*.js"],
    }),


    new webpack.DefinePlugin({ "global_a": JSON.stringify("我是一个打包配置的全局变量") }),
  ],

  resolve: {
    alias:{
      //  设置路径别名
      '@': path.join(config.root, 'src') ,

      '~':  path.join(config.root, './src/assets') ,
    },
    //  可互忽略的后缀
    extensions:['.JSX', '.js', '.json'],
    //  默认读取的文件名
    mainFiles:['index', 'main'],
  }
}

//  使用node.js的导出,将配置进行导出
module.exports = modules

.babelrc

代码语言:javascript复制
{
  "presets": [
    "@babel/preset-react",
    [
      "@babel/preset-env",
      {
        "modules":false
        //  移除useBuiltIns设置
        //      "targets": "chrome > 75",
        //      "useBuiltIns": "usage",
        //      "corejs": {
        //        "version": 3,
        //        "proposals":true
        //      }
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": {
          "version": 3,
          "proposals": true
        }
      }
    ]
  ]
}

0 人点赞