【前端第十三课】npm机制,掌握常用命令;webpack打包的基本含义;组件的生命周期;React路由

简介: 【前端第十三课】npm机制,掌握常用命令;webpack打包的基本含义;组件的生命周期;React路由

目标

了解npm机制,掌握常用命令

了解webpack打包的基本含义

掌握组件的生命周期

掌握React路由


React进阶

npm

npm(Node package manager)是随Node.js发布的包管理工具,最初主要是用来管理Node.js依赖包,如今已经扩展到整个JavaScript生态。


在没有 npm 之前,如果我们想要在自己的JS项目中引用第三方的框架或库,比如 React、Bootstrap 等等,需要把代码一个一个下载回来添加到自己的工程中,随着项目的扩大,依赖会变得越来越多,而第三方库可能也会有自己的依赖,一旦有版本升级,以来的管理很容易失控。


npm 的工作方式大致如下:


架设一个中心化的代码仓库服务器(registry),用来存放共享的代码,官方的npm网站为 https://www.npmjs.com/,在国内我们通常会使用阿里的 npm 镜像,下载速度会更快,切换方式如下

npm config set registry https://registry.npm.taobao.org

开源软件的作者将自己的代码封装成npm包(package),并且确定一个在registry中唯一的名字,如 react,然后将代码publish到registry


其他开发者想要使用 react 这个包,在自己的项目中运行 npm i react ,npm就会自动帮他们下载代码。


下载完成的代码会出现在项目根目录的 node_modules 目录


包也可以依赖npm上面其他的包,npm在安装的时候会自动解析、安装这些依赖


package.json

https://docs.npmjs.com/creating-a-package-json-file


package.json 是一个包或者工程的描述文件,里面记录了项目的名字、版本、依赖等信息,代表当前目录是一个独立的npm工程,一个基本的 package.json 内容大致如下:

{
  "name": "react-template",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  },
  "devDependencies": {
    "@types/react": "^17.0.5",
    "@types/react-dom": "^17.0.3"
  }
}

一些基本的字段意义如下:


name::包名,必填字段,不可以和依赖中的包名重复,如果将来要发布到npm仓库,需要保持在npm仓库中唯一

version:版本号,必填字段,遵循 semantic-versioning 规则

main:包入口代码文件,其他代码来引用此模块的时候,会自动引入此文件

license:开源协议

dependencies:生产环境依赖的包列表,通常是运行时依赖的库,会被安装在 node_modules 目录

devDependencies:开发环境依赖的包列表,通常是辅助开发构建用到的一些工具,比如 webpack,也会安装到 node_modules 目录

创建package.json

当我们想要初始化一个项目的时候,我们可以通过下面的步骤创建一个 package.json 文件来管理整个工程依赖


创建一个新的工程目录,如 react-template

进入该目录,执行命令 npm init ,按照提示一步一步填写信息

执行完毕之后,该目录会生成对应内容的 package.json,然后我们就可以在这个目录安装依赖了


npm scripts

在 package.json 里面有一个 scripts 属性,它可以添加一些自定义的命令,让我们非常方便地去执行一些构建程序,如

{
  "dev": "webpack --config scripts/webpack.config.js",
  "start": "webpack serve --config scripts/webpack.config.js --open",
  "build": "webpack --config scripts/webpack.prod.js"
}

对于其中定义的任务,我们可以直接通过 npm run <name> 来执行,如

npm run build

实际执行的就是

webpack --config scripts/webpack.prod.js

这里 webpack 是作为一个项目依赖安装到 node_modules 目录的,因为 webpack 提供了命令行的支持,我们在 npm scripts 中调用它时就可以正常找到它,而不需要专门安装成一个全局命令。


有一些名字是 npm 默认支持的,比如 start,我们可以直接使用 npm start 来调用。


安装依赖

如果我们要安装一个生产环境依赖,如 React, 可以在项目根目录(package.json所在目录)执行命令

npm i react

安装完之后,依赖关系会自动记录在 package.json 文件的 dependencies 中,如果我们要安装一个开发环境依赖,如 webpack,可以添加一个 --save-dev 参数,如

npm i webpack --save-dev

安装完之后,依赖关系会自动记录在 package.json 文件的 devDependencies 中,这两种方式安装的依赖,代码都会被下载到项目根目录的 node_modules 目录,除此之外还会生成一个名为 package-lock.json,这里面记录了整个已安装的依赖关系和下载地址,可以帮助我们锁定依赖版本。node_modules 通常比较庞大,通常来说当我们发布项目或者将项目代码提交到Git仓库时,需要将 node_modules 目录添加到忽略列表,只提交 package.json、package-lock.json 即可,其他人拿到代码之后只需要在项目根目录执行命令


npm i

npm就会根据 package.json、package-lock.json 中的记录自动帮你安装所需要的依赖模块


需要注意,在执行npm安装模块的命令时,一定要保证该目录存在正确的 package.json 文件,否则npm会一层一层往上递归寻找 package.json,直到分区根目录


全局安装

有些npm模块提供了命令行调用的支持,我们可以用 -g 来将它安装成一个全局命令,比如之前我们安装过的 typescript

npm i typescript -g

通过这种方式安装的模块安装为一个命令,比如 typescript 定义的命令为 tsc,我们就可以全局使用这个命令来编译TS代码了,需要注意的是,通过 -g 安装的模块并不会存在于本地项目的 node_modules 中,而是由npm全局管理,也不能在代码中直接引用。


删除依赖

如果我们想要删除一个已安装的依赖,如 react,只需要在 package.json 所在的目录执行命令

npm remove react

如果要删除一个全局安装的模块,需要再加上 -g 的参数,如


npm remove typescript -g

types

在我们使用TypeScript的过程中避免不了需要引用第三方的库,例如react,有些库自身就使用TS开发,发布到npm的时候也携带了对应的类型定义文件,对于这种库TS可以直接识别它的类型,但是大部分的库还是采用传统的JS开发,TS无法直接识别它的类型,所以TS采取了一种机制,可以为传统的JS模块编写类型定义文件,来描述他们的接口类型,然后通过npm仓库 @types/<name> 来发布,比如react是标准JS开发的,社区为它编写了TS类型定义模块,可以让TS识别react中的接口和数据类型,然后通过 @types/react 来发布,对应的地址为 https://www.npmjs.com/package/@types/react,我们前面的课程示例中也有用到它,可以用下面的方式来安装

npm i @types/react --save-dev

在以后的开发中,如果你要在TS中使用某一个第三方的npm模块,需要先看一下它自身有没有携带类型定义文件,如果没有的话,再去 @types 下面看一下有没有社区提供的版本,目前主流的常用库基本都覆盖了。


webpack

https://webpack.js.org/


上面我们学习了如何通过npm来安装、管理依赖,现在我们来看一下如何使用这些依赖。


npm最早是针对Node.js依赖管理设计的,后来随着 webpack 之类的打包工具的出现,让前端也可以利用npm生态来做模块依赖管理了。


在原生Modules出现之前,前端只能通过一些模拟的方式来实现模块化,webpack 最核心的功能是可以将依赖的模块代码进行转换,然后合并在一起,这样开发效率、加载效率都会有明显提升,例如我们可以用下面的方式来引用 react

import * as React from 'react'

有了npm之后,我们import导入的代码就会分为两种情况,一种是npm安装的模块,另外一种就是我们自己本地编写的代码,这两种的引用方式有所不同,对于npm安装的模块,我们引用的时候 from 后面直接写模块名,就像上面的引用 react 的方式

import * as React from 'react'import * as ReactDOM from 'react-dom'

对于本地模块代码,就是我们之前熟悉的方式了,通过相对路径的方式来引用,通过 webpack 的配置,我们可以省略掉 .js 或者 .ts 的后缀名,如

import Button from './components/Button'import Modal from './components/Modal'

除了JS代码之外,webpack通过加载器(loader)机制还可以用来处理其他类型的资源,例如图片、CSS、字体等等,还可以通过插件(plugin)机制来自动生成HTML模板、合并输出CSS文件等,我们通过demo的代码来看一下简单的webpack配置,webpack是基于Node.js开发的,它的配置文件就是一个Node.js脚本

const path = require('path')const HtmlWebpackPlugin = require('html-webpack-plugin')const MiniCssExtractPlugin = require('mini-css-extract-plugin')const { CleanWebpackPlugin } = require('clean-webpack-plugin')module.exports = {  entry: {    // 入口模块    index: './src/index.tsx'  },  output: {    // 输出路径    path: path.resolve('./dist'),    // 输出的文件名格式,这里采用名字+内容hash片段的方式,用于清理缓存    filename: 'scripts/[name].[contenthash:6].js',    // 添加公共路径,主要用于history路由模式下    publicPath: '/'  },  resolve: {    // 支持的脚本后缀名,可以让我们在导入的时候省略掉    extensions: ['.ts', '.tsx', '.js']  },  // 模式,development开发环境,production生产环境,会压缩代码  mode: 'development',  // 生成sourcemap  devtool: 'source-map',  module: {    // 加载器规则    rules: [      {        // 使用ts-loader处理ts|tsx后缀的代码        test: /\.(ts|tsx)$/,        exclude: /node_modules/,        loader: 'ts-loader'      },      {        // 使用MiniCssExtractPlugin.loader、css-loader处理css后缀的代码        test: /\.css$/,        use: [MiniCssExtractPlugin.loader, 'css-loader']      },      {        // 使用file-loader处理jpg|png|svg|ico格式的文件        test: /\.(jpg|png|svg|ico)$/,        loader: 'file-loader',        options: {          // 输出的文件名格式          name: 'imgs/[name].[contenthash:6].[ext]'        }      }    ]  },  plugins: [    // 清理插件,可以在每次编译之前清空dist目录    new CleanWebpackPlugin(),    // 可以将引入的样式合并输出到css文件    new MiniCssExtractPlugin({      filename: 'styles/[name].[contenthash:6].css'    }),    // 根据模板生成html文件    new HtmlWebpackPlugin({      filename: 'index.html',      template: './src/index.html'    })  ]}

loader

https://webpack.js.org/loaders/


在我们的代码中会引用很多不同类型的资源,例如TS、图片、CSS,我们可以像引用JS模块一样来引用他们,例如


import './style.css'import logo from '../imgs/logo.png'import Button from '../components/Button.tsx'

但是很明显,这几种都不是合法的JS模块,无法被正常执行,这时候就需要webpack的各种loader来帮我们处理这些类型的文件,以上面的代码为例,我们希望import进来的css最终输出成css文件,并且自动插入到html页面中,这里我们用到了两个loader,分别是 MiniCssExtractPlugin.loader 和 css-loader,webpack会从右往左来调用指定的loader,其中 css-loader 可以获得css路径、内容等信息,MiniCssExtractPlugin.loader 可以用来将css内容导出到文件。


对于图片类型如png,我们使用了 file-loader,webpack在打包代码的时候如果遇到导入png,就会把这个任务交给 file-loader,此时它会按照我们配置的格式,将文件输出到指定位置,并返回新的路径名,上面我们得到的 logo 就是输出之后该图片的路径,这样我们就可以在JSX中插值引用该图片了。


对于TS代码,我们使用把它交给了 ts-loader 来处理,它会帮我们自动完成TS到JS的编译。


经过上面几个loader对不同资源的处理,我们得到了可以在浏览器中正常执行的代码。loader也是一个npm模块,需要通过npm来安装


plugin

https://webpack.js.org/plugins/


loader用来处理不同类型的资源,而plugin可以用来扩展webpack的功能,webpack内置了一些插件,也有很多社区提供的,上面的例子中我们使用了三个插件


CleanWebpackPlugin:这个插件可以帮助我们清理输出目录,避免混入旧版本的文件

MiniCssExtractPlugin:这个插件让我们把导入的css代码合并输出到css文件

HtmlWebpackPlugin:这个插件可以根据模板来生成我们的入口html文件,而且会自动把输出的JS、CSS文件引入进来

webpack-dev-server

webpack-dev-server 是一个小型嵌入式的Web服务器,它可以和webpack配合使用,能够支持静态文件、API代理、自动刷新等功能,通过它我们可以在开发环境给每一个前端工程启动一个独立的http server,方便我们调试,而部署到生产环境的时候再使用Nginx。webpack-dev-server 的配置格式大致如下,可以直接添加到webpack的配置文件:

devServer: {  // 静态文件目录  contentBase: path.resolve('./dist'),  // 自动刷新  hot: true,  // 监听端口  port: 3360,  // 支持history路由模式  historyApiFallback: true,  // API代理  proxy: {    '/data': {      target: 'https://data.example.com',      changeOrigin: true    }  }}

添加完配置之后,我们可以通过 npm scripts 来定义一个启动服务的命令,如


"scripts": {  "start": "webpack serve --config scripts/webpack.config.js --open"}

然后我们执行命令 npm start 就可以启动http服务了,也会自动打开默认浏览器进行访问,当我们修改了代码引起输出文件的变化时,会通知浏览器自动刷新。


webpack-dev-server 默认会将webpack输出的文件写入到内存中,而不是磁盘中,所以在启动它的时候,dist目录是没有输出文件的。


react有一个著名的脚手架工具 create-react-app,有兴趣的同学以后可以自己研究一下,不在本课程的讨论范围。


组件的生命周期

https://react.docschina.org/docs/react-component.html


一个React组件从初始化到被删除,会经历一个完整的生命周期,我们可以为类组件添加一些特殊方法,在组件生命周期的的一些关键节点,React会来执行这些方法。


componentDidMount()

componentDidMount() 会在组件挂载之后(插入DOM树中)立即调用,依赖于DOM节点的初始化操作应该放在这里,通常我们会在这里通过网络获取组件所需的数据、订阅一些事件通知等等


componentDidUpdate()

componentDidUpdate() 会在更新后被立即调用,首次渲染不会执行此方法,可以让我们监控组件传入数据发生的变化。


componentWillUnmount()

componentWillUnmount() 会在组件卸载及销毁之前直接调用,在此方法中执行必要的清理操作,例如:清除定时器,取消网络请求或者之前在 componentDidMount() 中创建的事件订阅等。在此方法中不应该调用 setState(),因为该组件将永远不会重新渲染,组件实例卸载后,将永远不会再挂载它。


路由

https://reactrouter.com/web/guides/quick-start


目前我们所接触到的react例子都是单页面的,只有一个页面组件,在实际项目中通常都会有多个页面存在,可以通过链接来进行切换,达到多页面的效果。要实现这个功能,我们需要借助于React的路由模块 react-router-dom 来实现,它可以让我们监听浏览器地址的变化,并且解析这个URL对象,然后router根据URL的路径匹配到路由对应页面组件,最后正确地渲染对应地组件,这个就是路由工作的基本原理。


我们平时会使用的主要是两种路由模式:


HashRouter:这种模式基于浏览器location的hash片段来实现,实现比较简单,不需要服务器的支持,缺点是url样式不够优雅,而且hash参数容易丢失,如下:

http://example.com/#/home/files

BrowserRouter:这种模式基于浏览器的history API,可以让我们创建一个像 http://example.com/home/files 这样真实的URL,而且切换url不会引起页面的刷新,用户体验比较好,是我们比较推荐的路由方式,不过这种模式需要服务器比如Nginx的支持,因为路径 /home/files 只是一个前端定义的路由,当用户刷新页面的时候浏览器会去向服务器请求这个资源,服务器因为没有对应的这个资源,就会返回404,导致页面无法显示,所以需要Nginx将所有404的请求返回入口文件 /index.html,大概配置如下

location / {  root d:/www;  try_files $uri $uri/ /index.html;}

路由的定义

我们可以把React组件分为路由组(页面)件和普通组件,路由组件对应的就是一个路由页面,我们可以用下面的方式来定义一个路由组件 About

import * as React from 'react'import { RouteComponentProps, Link } from 'react-router-dom'export default class About extends React.Component<RouteComponentProps> {  render() {    return (      <h1>About</h1>    )  }}

这里我们要给组件添加一个props的泛型 RouteComponentProps,添加之后TS就可以识别到传入的和路由控制相关的props参数,具体参考示例代码


然后我们需要定义一个根组件 App,用来确定路由和组件的映射关系,如下

import * as React from 'react'import { BrowserRouter, Route, Switch } from 'react-router-dom'import Main from '../Main'import About from '../About'export default class App extends React.Component {  render() {    return (      <BrowserRouter>        <div className="container">          <Switch>            <Route path="/" exact component={Main} />            <Route path="/about" component={About} />          </Switch>        </div>      </BrowserRouter>    )  }}

在上面的代码中我们定义了两个路由规则,放在了 Switch 中,路由规则的定义通过 Route 组件来进行,它有两个关键的参数,分别是:


path:要匹配的URL路径

component:该路由对应的页面组件

需要注意的是,path的路径默认会匹配到子路径,例如 /detail 也会匹配到 /detail/123,如果不像匹配子路径,需要加上 exact 属性。


Switch 会根据当前的URL路径匹配它下面的 Route,然后将匹配到的第一个组件渲染到当前位置。


带参数的路由

有时候我们的页面会接收一些参数,比如根据传入的关键词去进行搜索,react-router-dom 支持路由参数,我们可以用下面的方式来定义

<Route path="/search/:keyword" component={Search} />

然后我们可以在对应的路由组件 Search 中来接收这些参数,通过下面的方式来进行


首先我们需要定义合适的 Props 类型,RouteComponentProps 也支持传入参数泛型,如下

interface Params {  keyword: string}type Props = RouteComponentProps<Params>export default class Search extends React.Component<Props> {  render() {    return (      <div>{this.props.match.params.keyword}</div>    )  }}

我们首先定义了当前路由组件接收的参数类型 Params,跟我们在 Route 中定义的参数一致,然后我们可以在组件内通过 this.props.match.params 来访问到我们的路由参数对象,它的类型就是 Params


路由导航

我们可以使用两种方式来在页面中切换路由,react-router-dom 提供了一个 Link 组件,我们可以用它来替代传统的a标签,因为我们使用的 history 模式的 BrowserRouter,它其实是利用浏览器的 history.pushState 来实现的,它可以在浏览器添加历史状态,修改当前的URL而不会引起页面的刷新,Link 可以帮我们拦截用户的点击,然后完成 pushState 相关的操作,这样就可以在切换路由的同时保持页面的状态,而不是刷新页面。它的基本用法如下:

import { Link } from 'react-router-dom'export default class Main extends React.Component {  render() {    return <Link to="/about">About</Link>  }}

参数 to 就是要导航到的路由,需要注意,Link标签只用来切换应用内的路由,如果你想要跳转到外部链接,例如 MDN,那你应该使用传统的a标签,如

<a href="https://developer.mozilla.org/zh-CN/">MDN</a>

还有一种方式是通过API来进行导航,对于路由组件,react-router-dom 会通过 props 传入一些数据和API,我们可以通过调用下面的API来进行路由导航

this.props.history.push('/about')

需要注意的是,this.props.history 只在路由组件内部才可以使用,如果你想要在普通组件内部也能访问路由信息,需要使用 withRouter 来包裹一下,例如

import * as React from 'react'import { Link, withRouter, RouteComponentProps } from 'react-router-dom'class Nav extends React.Component<RouteComponentProps> {  componentDidMount() {    console.log(this.props.history)  }  render() {    return (      <nav className="nav">        <Link to="/" className="nav-item">          Main        </Link>        <Link to="/about" className="nav-item">          About        </Link>      </nav>    )  }}export default withRouter(Nav)

这样我们就可以访问路由信息了


NavLink

react-router-dom 还提供了一个特殊的Link组件叫 NavLink,它可以让我们来创建一些导航元素,当前的路由如果和它的链接匹配的话,可以自动帮我们添加选中的状态,例如上面的 Nav 组件我们可以用 NavLink 来替换一下

import * as React from 'react'import { NavLink, withRouter, RouteComponentProps } from 'react-router-dom'class Nav extends React.Component<RouteComponentProps> {  render() {    return (      <nav className="nav">        <NavLink to="/" className="nav-item" exact activeClassName="nav-active">          Main        </NavLink>        <NavLink to="/about" className="nav-item" activeClassName="nav-active">          About        </NavLink>      </nav>    )  }}export default withRouter(Nav)

当路由和 NavLink 中的 to 匹配时,会自动添加一个 nav-active 类名

相关文章
|
1月前
|
监控 前端开发 数据可视化
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
@icraft/player-react 是 iCraft Editor 推出的 React 组件库,旨在简化3D数字孪生场景的前端集成。它支持零配置快速接入、自定义插件、丰富的事件和方法、动画控制及实时数据接入,帮助开发者轻松实现3D场景与React项目的无缝融合。
115 8
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
|
24天前
|
移动开发 缓存 前端开发
深入理解前端路由:原理、实现与应用
本书《深入理解前端路由:原理、实现与应用》全面解析了前端路由的核心概念、工作原理及其实现方法,结合实际案例探讨了其在现代Web应用中的广泛应用,适合前端开发者和相关技术人员阅读。
|
1月前
|
缓存 前端开发 JavaScript
JavaScript前端路由的实现原理及其在单页应用中的重要性,涵盖前端路由概念、基本原理、常见实现方式
本文深入解析了JavaScript前端路由的实现原理及其在单页应用中的重要性,涵盖前端路由概念、基本原理、常见实现方式(Hash路由和History路由)、优点及挑战,并通过实际案例分析,帮助开发者更好地理解和应用这一关键技术,提升用户体验。
74 1
|
2月前
|
JavaScript 前端开发 Docker
前端全栈之路Deno篇(二):几行代码打包后接近100M?别慌,带你掌握Deno2.0的安装到项目构建全流程、剖析构建物并了解其好处
在使用 Deno 构建项目时,生成的可执行文件体积较大,通常接近 100 MB,而 Node.js 构建的项目体积则要小得多。这是由于 Deno 包含了完整的 V8 引擎和运行时,使其能够在目标设备上独立运行,无需额外安装依赖。尽管体积较大,但 Deno 提供了更好的安全性和部署便利性。通过裁剪功能、使用压缩工具等方法,可以优化可执行文件的体积。
151 3
前端全栈之路Deno篇(二):几行代码打包后接近100M?别慌,带你掌握Deno2.0的安装到项目构建全流程、剖析构建物并了解其好处
|
1月前
|
前端开发 JavaScript 开发者
揭秘前端高手的秘密武器:深度解析递归组件与动态组件的奥妙,让你代码效率翻倍!
【10月更文挑战第23天】在Web开发中,组件化已成为主流。本文深入探讨了递归组件与动态组件的概念、应用及实现方式。递归组件通过在组件内部调用自身,适用于处理层级结构数据,如菜单和树形控件。动态组件则根据数据变化动态切换组件显示,适用于不同业务逻辑下的组件展示。通过示例,展示了这两种组件的实现方法及其在实际开发中的应用价值。
44 1
|
2月前
|
缓存 前端开发 JavaScript
前端serverless探索之组件单独部署时,利用rxjs实现业务状态与vue-react-angular等框架的响应式状态映射
本文深入探讨了如何将RxJS与Vue、React、Angular三大前端框架进行集成,通过抽象出辅助方法`useRx`和`pushPipe`,实现跨框架的状态管理。具体介绍了各框架的响应式机制,展示了如何将RxJS的Observable对象转化为框架的响应式数据,并通过示例代码演示了使用方法。此外,还讨论了全局状态源与WebComponent的部署优化,以及一些实践中的改进点。这些方法不仅简化了异步编程,还提升了代码的可读性和可维护性。
|
2月前
|
JSON 前端开发 JavaScript
前端模块打包器的深度解析
【10月更文挑战第13天】前端模块打包器的深度解析
|
2月前
|
存储 前端开发 JavaScript
前端模块化打包工具的深度解析
【10月更文挑战第13天】前端模块化打包工具的深度解析
|
2月前
|
前端开发 JavaScript 开发工具
从零开始:构建、打包并上传个人前端组件库至私有npm仓库的完整指南
从零开始:构建、打包并上传个人前端组件库至私有npm仓库的完整指南
412 0
|
2月前
|
JavaScript 前端开发 应用服务中间件
Vue开发中,在实现单页面应用(SPA)前端路由时的hash模式和history模式的区别及详细介绍
Vue开发中,在实现单页面应用(SPA)前端路由时的hash模式和history模式的区别及详细介绍
48 0

热门文章

最新文章