在项目开发过程中,我们经常能看到各种特殊格式的代码注释,例如可能会以 @
开头,或者是格式很复杂,甚至编辑器还会给这些注释加上代码高亮。
这些就是被称为 “Magic comment”(魔法注释)的特殊注释。这些注释一般不会对代码本身的运行产生影响,但是,编译工具(例如 Webpack、TypeScript)或 IDE 则会读取它们,并产生一些帮助或优化。
常见的 JSDoc 注释
经常能见到的注释格式,这里以 TypeScript 举例:
/**
* 拉取指定用户的信息
*
* @param userId 用户 ID
* @return 用户信息对象
*/
function fetchUserInfoApi(userId: string): IUserInfo {
return {}
}
我们经常能在项目中看到这类注释。它通常写在方法上方,标注了方法的参数、返回值等信息,还会有一些文本介绍。通常来说,库的作者都会提供尽可能详细的注释,便于使用者理解。
想要用键盘输入这种注释,只需要输入 /**
开头,并按下回车键,编辑器就会自动帮我们生成注释。
我们还可以注意到,这种注释也有文本高亮,所以是一种具有特定格式的注释:
打出这种注释后,我们在其他地方想要使用这个方法,在输入的时候,编辑器也会自动给我们提示:
这种注释的名称叫 JSDoc。
在以前没有 TypeScript 的年代,JavaScript 开发者们约定使用这种特殊格式的注释,用于标记一个方法的参数、返回值等信息,而编辑器、IDE 也会集成解析 JSDoc 的功能,在我们调用方法时展示出相关的提示。
这里有两点需要特别注意:只有 /**
开头的注释会被识别为 JSDoc 注释,JSDoc 注释即使只有一行也是可以的,但以 //
和 /*
开头的都不行;此外,JSDoc 的语法支持 markdown 格式,VSCode 可以正确解析和显示。
JSDoc 的结构看上去很直观,开头一段是介绍文本,然后是很多行由 @params
这种开头格式的标识符,用于标识一个方法的参数、返回类型等。JSDoc 不仅可以给方法用,也可以给对象、类、变量使用,它也支持很多种标识符,具体可以前往 官方文档 查看和参考。
上面的例子是 TypeScript 的注释。
如果你的项目没有使用 TypeScript,那么可以写成这种格式:
/**
* 拉取指定用户的信息
*
* @param {string} userId 用户 ID
* @return {Object} 用户信息对象
*/
function fetchUserInfoApi(userId) {
return {}
}
使用花括号格式,来标注参数的类型。
JSDoc 示例
这里给出几段 JSDoc 用法示例,以下均不使用 TypeScript:
下面是一个构造函数的 JSDoc 用法,其中用到了构造函数的标识 @constructor
,这个方法的 country
参数是可选的,JSDoc 中表示为方括号包裹起来 [country]
这种形式:
/**
* 构造一个 Person 对象
*
* @constructor
* @param {string} name 名字
* @param {number} age 年龄
* @param {string} [country] 国家(可选)
*/
function Person(name, age, country) {
this.name = name;
this.age = age;
this.country = country || 'Unknown';
}
下面是一个带有使用示例的 JSDoc,使用 @example
作为标识符:
/**
* 计算两个数的和
*
* @param {number} a 加数
* @param {number} b 被加数
* @returns {number} 计算出的结果
*
* @example
*
* // 得到结果: 7
* sum(3, 4)
*/
function sum(a, b) {
return a + b;
}
下面是一个使用对象作为参数的 JSDoc 示例:
/**
* 提交用户信息
*
* @param {Object} userInfo 提交的用户对象
* @param {string} userInfo.name 用户的姓名
* @param {number} userInfo.age 用户的年龄
* @param {Date} userInfo.createAt 用户的创建日期
*/
function submitUserApi(userInfo) {}
使用这种格式,在调用方法时候,编辑器也能正确的给出格式提示:
下面是一个使用复杂类型参数的 JSDoc 示例:
/**
* 我是复杂参数的方法
*
* @param {string[]} arrayParam 我是字符串数组
* @param {(string|number)} mixedParam 我可以是数字或字符串
* @param {*} anyParam 我可以是任何类型
*/
function test(arrayParam, mixedParam, anyParam) {}
在调用这个方法时,编辑器可以从 JSDoc 中推导出参数的类型,从而给我们提供提示:
可以看出,JSDoc 主要是在没有 TypeScript 的年代大放光彩。如果你还在使用 JavaScript,那么可以参考 TypeScript 提供的一份 文档,它提供了 JSDoc 来模拟 TypeScript 功能的方式。
如果你的项目使用了 TypeScript,那么 JSDoc 的作用就会减弱很多,因为 TypeScript 本身具备类型标注的功能。
但是,我们还是可以利用 JSDoc 的编辑器提示功能,在复杂的方法上方加上以 /**
开头的说明注释,这样别人才调用你的方法时,就可以看到你写的说明注释。之前的 //
双斜杠注释是不会显示编辑器的提示的,要改掉这个习惯。
TSDoc
还有一种升级版的 JSDoc,叫 TSDoc,目前 VSCode 已经原生支持。
在用户已经使用了 TypeScript 时,便可以减少 JSDoc 中啰嗦的类型标注,格式有所变化。
这里给出一个 TSDoc 的片段示例:
/**
* 用 `<div>` 包裹元素
*
* @param {import("react").ReactNode} input 提供的节点
* @returns {import("react").ReactNode} 返回被包裹的节点
*/
function wrapperDiv(input) {
return <div>{input}</div>
}
可以看到,这里可以使用 import()
这种方式来导入类型。此外,TSDoc 还支持如 @typedef
、@template
等多种标注。
TypeScript 的 @ts-
注释
常见的 TypeScript 注释一共有四种:
// @ts-ignore
取消下一行代码的 TypeScript 类型检查错误提示。
这个是最常用的,如果下一行代码出于某些原因类型问题暂时无法解决,可以使用此方式来取消类型检查提示。
// @ts-expect-error
预期下一行代码存在至少一个错误,因此先在此标记一下,暂时不要提示。
这个功能和上面的 // @ts-ignore
类似,但是相比来说,这个显然更好一点,因为语义更加明确,并且如果未来代码更新了,下一行语句的错误不存在了,编辑器会把这个注释标红,便于我们及时获知代码的变动。
可以看到,因为不存在错误了,所以 // @ts-expect-error
出现了红色波浪线提示,因此它其实是 // @ts-ignore
的更好的替代。
// @ts-nocheck
只能放在文件首行,表示不对此文件进行类型检查。
一般来说,在 TypeScript 项目中引入 JS 的文件(最常见的场景是引入 JS 的 SDK),通常这会导致整个文件有一大堆报错,使用这个注释,便可以让此文件的错误不再显示。
同样的,// @ts-check
只能放在文件首行,一般用于 .js 文件,表示为此文件开启类型检查。
这些 TypeScript 注释,支持 //
或 /* */
这两种注释格式。
对于 React 等项目而言,也支持在 JSX 中使用:{/* @ts-ignore */}
。
Webpack 的 /*#__PURE__*/
注释
首先明确一下,/*#__PURE__*/
注释是由 Webpack 在编译构建过程中添加的,用于给其它工具提供代码标注,便于其他工具更好的处理代码;并不需要由开发者来写。打包构建后的代码中,这些注释可能已经被移除了。
在 Webpack 4 以及之前,Uglify.js 会读取并使用这些注释;而在 Webpack 5 以及后续版本,则是由 Terser 读取并使用这些注释。
Webpack 有一个名叫 “TreeShaking” 的功能,它可以分析并在打包时删除没有使用到的代码,这个过程就是由 Terser 来进行的,因此此注释实际上是给 Terser 使用的。
简单介绍一下 TreeShaking:
例如一个模块里导出了 3 个方法,但是只被使用了 1 个,那么 TreeShaking 可以使打包产物不包含其他 2 个方法的代码,以此来减小打包后的代码体积。因此工具要分析代码中方法、变量的使用情况,确保只有未被使用的方法、变量才被 TreeShaking 删除,否则会引起代码的功能异常。
通常来说,变量的使用情况分析起来较为容易;而方法的使用情况则涉及到一个 “副作用” 的概念:
如果一个方法只是单纯的接受一个输入并返回一个结果输出,它并不修改输入的变量,更不会去修改外部变量,那么这个方法可以被看做是 “纯净的”、“无副作用的”。
请看以下示例:
文件 convertUserInfo.ts
:
// 假设此方法只是单纯的做对象字段转换
// 它不会修改传入的变量,也不会引起任何外部改动
export function convertUserInfo(userInfo: IUserInfo): INewUserInfo {
return { ...userInfo, name: userInfo.username }
}
以上代码中,convertUserInfo
是一个完全没有副作用的方法,它接收一个变量,并返回一个新变量,并不会对原来的变量进行修改,也不会对方法外部做任何改动。
页面文件:
import { convertUserInfo } from './convertUserInfo'
export default function UserInfoPage() {
const userInfo = useUserInfo()
// 注意下面这一行,newUserInfo 并没有被使用
const newUserInfo = convertUserInfo(userInfo)
return <div>{userInfo.name}</div>
}
可以看到,const newUserInfo = convertUserInfo(userInfo)
这一句代码,方法返回的变量并没有被使用,我们可以认为:这一行语句可以被 TreeShaking 删除掉;甚至如果 convertUserInfo
方法仅有这一处调用,那么 convertUserInfo.ts
这整个文件都可以丢弃掉不必打包。
而以上代码在打包之后(注意请以打包构建后的产物为准),产物中还是会包含 convertUserInfo
相关的代码,因为工具认为 convertUserInfo
方法被引入且使用了,这个方法有可能有副作用,所以不能删掉它。
修改文件页面文件的代码为:
import { convertUserInfo } from './convertUserInfo'
export default function UserInfoPage() {
const userInfo = useUserInfo()
// 注意下面这一行,添加了注释
const newUserInfo = /*#__PURE__*/ convertUserInfo(userInfo)
return <div>{userInfo.name}</div>
}
上述代码中,添加了 /*#__PURE__*/
注释之后再打包代码,构建产物中这一行代码就不存在了;如果 convertUserInfo
方法全局仅这里使用了一次,那么 convertUserInfo.ts
整个文件都不会被打包。
由此可见,/*#__PURE__*/
注释可以标注方法调用是 “纯净的”、“无副作用” 的。
被此注释标注的方法调用,如果它的返回值没有被使用,那么这些语句便可以被 TreeShaking 删除。
其实,/*#__PURE__*/
也可以写成 /*@__PURE__*/
的格式,Terser 的注释支持符号 #
和 @
开头。
此外,Terser 还支持以下几种注释(开头的 #
也都可以换成 @
),可在 官方文档 处查看:
注释 /*#__INLINE__*/
标注一个方法调用,确保此方法被内联;
注释 /*#__NOINLINE__*/
标注一个方法调用,确保此方法不被内联。
Terser 中具备变量和方法的 “内联” 的功能,配置项 compress.inline
便是控制此行为的(文档),此配置项取值:
0
或false
表示关闭内联;1
表示只内联简单的方法;2
表示内联方法和参数(这也是create-react-app
的默认配置);3
或true
表示内联方法和参数以及变量(这也是 Terser 的默认值)。
以 create-react-app
创建的默认项目为例,页面文件:
function addOne(input: number) {
return input + 1
}
function addTwo(input: number) {
return input + 2
}
function App() {
console.log(addOne(111))
console.log(addTwo(222))
console.log(addTwo(333))
return null
}
export default App
然后运行 yarn build
打包;
在打包后的产物中搜索 111
、222
,可以看到如下代码:
function r(e) {
return e + 2
}
var l = function () {
return console.log(111 + 1), console.log(r(222)), console.log(r(333))
}
可以看到,在构建产物中,如果方法只被用过一次,那么方法代码将被直接 “内联” 到调用方,也就是第一段 console.log(111 + 1)
,这里直接把 addOne
的源代码复制到此处了;而 addTwo
方法被多次使用,它没有被 “内联”。
如果将上述代码修改为:
function addOne(input: number) {
return input + 1
}
function addTwo(input: number) {
return input + 2
}
function App() {
console.log(/*#__NOINLINE__*/ addOne(111))
console.log(/*#__INLINE__*/ addTwo(222))
console.log(/*#__INLINE__*/ addTwo(333))
return null
}
export default App
打包后的产物中,可以找到这一段代码:
function r(e) {
return e + 1
}
var l = function () {
return (
console.log(r(111)),
console.log(222 + 2),
console.log(
(function (e) {
return e + 2
})(333)
),
null
)
}
可以看到,原来的 addOne
被 /*#__NOINLINE__*/
给强制禁用内联了,虽然它只被调用了一次,但是也必须在外部声明方法;
而原来的 addTwo
被 /*#__INLINE__*/
给强制开启内联了,每次调用都需要把方法里的代码搬过来完整执行一遍,只不过为了避免变量重名,从第二次开始每次调用都会把方法代码包在一个 IIFE 函数里面。
注释 /*#__KEY__*/
:一般用于标注字符串,此字符串将被视为某个对象的 “属性名”,当对象的属性名被压缩时,此字符串也会一同改为对应的值,使用示例:
import _ from 'lodash'
_.get({ propA: 123 }, /*#__KEY__*/ 'propA')
Webpack 的 webpack*:
注释
Webpack 支持使用代码中的 “魔法注释(Magic Comments)” 来定义编译构建时的行为,可以参考 官方文档 上的说明。
其实日常开发中,我们最常见最常用的还是 /* webpackChunkName: "chunk名" */
这种用法。
注意:这些注释都是用于 “导入解析” 的场合,也就是我们常说的 “动态导入”,也就是 import()
这种写法。如果你用不到这种导入方式,那么可以直接跳过这个章节。
此章节中所有注释都需要放置在 import()
的括号中。
webpackChunkName
/* webpackChunkName: "chunk名" */
:在 Webpack 进行代码分割时,使相同的 chunkName 的代码合并打包到同一个文件里,并使用你提供的 chunkName 作为文件名。
这个是最常见最常用的注释,它用于在 Web 项目中,在使用 import()
来实现代码分割的同时来指定分割后的代码文件的名称。
介绍一下什么是代码分割:
现在 Web 项目通常都包含数十个甚至上百个页面,其中还会使用大量的图片、CSS、JS 等资源。而 SPA 单页站点会把资源打包整合成一个文件,一次性加载所有资源,此时网络带宽将直接影响到网页的加载速度,有可能导致长时间的白屏,最终影响到用户体验。
解决方案就是使用 Webpack 提供的 “代码分割” 功能,把项目代码分为几块,例如作为一个电商网站,可以把首页、商品详情页两个最频繁访问的页面放在一起首屏就加载,而其他页面单独放在其他的文件里,用户访问到时再去加载;甚至可以再极端一点,除了首页,其他所有页面都做成访问时再加载。
Webpack 提供的代码分割功能需要我们使用 ES Module 规定的 import()
方法来导入模块,打包时遇到这种代码,Webpack 便会将导入的模块拆分开,放在另一个 JS 文件中。一般来说,我们会在路由配置文件中来做这一步。
我们这里直接用 PaperPlane App 应用站的 部分源码 来举例,这里使用的是 react-router@6
,以这篇博文编写的时间点来看,这是一个非常新的版本:
import loadable from '@loadable/component'
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router-dom'
// 注意下面这两个页面/组件,它们是直接导入的
import MainLayout from '@/components/layout/main'
import HomePage from '@/pages'
// 下面这些页面,都是通过 import() 来导入的
const ToolsPage = loadable(() => import('@/pages/tools'))
const DingtalkPage = loadable(() => import('@/pages/tools/dingtalk'))
const WeeklyPage = loadable(() => import('@/pages/tools/weekly'))
const GPTChatPage = loadable(() => import('@/pages/tools/gptchat'))
const GitPage = loadable(() => import('@/pages/tools/git'))
const routerConfig: RouteObject[] = [
{
element: <MainLayout />,
children: [
{ path: '/', element: <HomePage /> },
{ path: '/tools', element: <ToolsPage /> },
{ path: '/tools/dingtalk', element: <DingtalkPage /> },
{ path: '/tools/weekly', element: <WeeklyPage /> },
{ path: '/tools/gptchat', element: <GPTChatPage /> },
{ path: '/tools/git', element: <GitPage /> },
],
},
]
const router = createBrowserRouter(routerConfig, { basename: process.env.PUBLIC_URL })
export default function RouterEntry() {
return <RouterProvider router={router} />
}
从上面的路由配置可以看出,除了首页之外,其他页面都是使用 import()
导入的,这样我们的项目便实现了代码分割。
如果用户访问我们的站点首页,那么只会加载布局组件 MainLayout
和首页 HomePage
两部分的代码,其他页面的代码不会立刻加载,而是等到用户访问对应页面的时候再去加载。
我们可以运行编译构建,查看一下构建产物:
可以看到代码被拆分成了很多块。
如果不使用 import()
组织代码,我们把其中的 import()
部分改为:
import ToolsPage from '@/pages/tools'
import DingtalkPage from '@/pages/tools/dingtalk'
import GitPage from '@/pages/tools/git'
import GPTChatPage from '@/pages/tools/gptchat'
import WeeklyPage from '@/pages/tools/weekly'
此时再运行编译构建,查看构建产物:
此时就只有一个代码文件,它的体积非常大,如果用户的网速不理想,加载这么大一个文件一定会引起长时间的白屏。
介绍完了代码分割,接下来介绍这个 /* webpackChunkName: "chunk名" */
注释的作用。
它需要配合 import()
方法来使用。最常见的用法是这样:
const ToolsPage = loadable(() => import(/* webpackChunkName: "app-tools" */ '@/pages/tools'))
const DingtalkPage = loadable(() => import(/* webpackChunkName: "app-dingtalk" */ '@/pages/tools/dingtalk'))
const WeeklyPage = loadable(() => import(/* webpackChunkName: "app-weekly" */ '@/pages/tools/weekly'))
const GPTChatPage = loadable(() => import(/* webpackChunkName: "app-gptchat" */ '@/pages/tools/gptchat'))
const GitPage = loadable(() => import(/* webpackChunkName: "app-git" */ '@/pages/tools/git'))
如上面代码示例所示,这个注释写在 import()
里面,冒号后面的字符串可以自己定。
我们把代码改成上面的格式,然后编译构建,看看产物:
可以看出,打包产物中的 JS 文件,都会使用我们前面注释中所写的名称来作为文件名。项目的入口文件名,固定是 main
。当然这里还会多出几个文件,它们是被多个文件公用的部分,被 Webpack 抽出来放到单独的 JS 文件中了。
看到这里,难道说这个注释的作用只是为了给打包后的文件重新命名吗?其实并不是,它真正的作用是令我们可以给多个导入的模块指定相同的文件名,这样这些文件便可以被打包到同一个文件中去,加载时也是同时加载。
以上面的项目代码来举例,作为用户,我们在上班时有可能会同时使用钉钉功能和周报功能,因此我想让 DingtalkPage
和 WeeklyPage
两个页面放在一个代码包里,这样它们之间页面跳转就是秒跳,不需要白屏等待;同样我想让 GPTChatPage
和 GitPage
两个页面放在同一个代码包里。
这样修改代码:
// 下面这两行,chunk 名都叫:app-work
const DingtalkPage = loadable(() => import(/* webpackChunkName: "app-work" */ '@/pages/tools/dingtalk'))
const WeeklyPage = loadable(() => import(/* webpackChunkName: "app-work" */ '@/pages/tools/weekly'))
// 下面这两个,chunk 名都叫:app-ai
const GPTChatPage = loadable(() => import(/* webpackChunkName: "app-ai" */ '@/pages/tools/gptchat'))
const GitPage = loadable(() => import(/* webpackChunkName: "app-ai" */ '@/pages/tools/git'))
此时编译构建项目,查看产物:
此时 Webpack 便会自动组织和打包代码,让我们所需的几个模块按照相同的 chunkName 组合在一起放到一个文件里面。
我们一开始说的需求:“例如作为一个电商网站,可以把首页、商品详情页两个最频繁访问的页面放在一起首屏就加载,而其他页面单独放在其他的文件里,用户访问到时再去加载”,便可以使用这种方式来实现。
对了,如果你的 import()
使用了字符串拼接的方式,也就是动态导入,Webpack 提供了两个占位符 [request]
和 [index]
供我们在 chunkName 中使用,它们分别表示 “文件实际的名称” 和 “自增的数字”。示例:
const animalName = 'cat'
import(/* webpackChunkName: "[request]" */ `./animals/${animalName}`)
在 Webpack 打包时,这里的 [request]
会被替换成目录 ./animals/
下的每个文件的实际名称。
webpackInclude
/* webpackInclude: /^正则$/ */
和 /* webpackExclude: /^正则$/ */
:分别用于指定哪些文件需要被包含、哪些文件需要被排除,需要提供一个正则表达式,用来匹配文件路径和文件名。
注意这个正则表达式是匹配被导入文件的完整路径的,你可以理解成从 /src
开始的完整路径。
举一个简单的例子即可说明:
const animalName = userInput
// 导入 ./animals/ 目录下的文件,但是,以 dog 开头的除外
import(/* webpackExclude: /\/dog/ */ `./animals/${animalName}.png`)
以上示例中,Webpack 会把 ./animals/
目录下所有 .png
结尾的文件都作为资源文件,但排除了任何以 dog
开头的文件。
用户输入一个名称,便会寻找对应名字的图片加载,找不到时会报错。
注意:如果资源是一个数组格式,并且通过 forEach
、map
等方式全部加载,使用这个注释会导致被排除的资源加载失败并报错,因此需要也在代码里控制,跳过被这个注释排除掉的资源文件的使用。举例:
import { useEffect, useState } from 'react'
// 注意,这里的文件列表里有一个 "aaa" 文件
const imageNameList = ['aaa', 'bbb', 'ccc']
function App() {
const [images, setImages] = useState<string[]>([])
useEffect(() => {
Promise.all(
// 注意,下面排除了 aaa 文件,所以代码一定会报错
imageNameList.map(imageName =>
import(/* webpackExclude: /aaa/ */ `./images/${imageName}.jpg`)
.then(fileInfo => fileInfo.default)
)
).then(setImages)
}, [])
return (
<div>
{images.map(image => (
<img src={image} alt="" key={image} />
))}
</div>
)
}
export default App
如果代码中需要引入的某些文件被 /* webpackExclude: /^正则$/ */
注释排除了,代码是一定会报加载资源失败的报错的,需要妥善处理。
webpackMode
/* webpackMode: "模式" */
:指定 Webpack 在加载模块时的模式。它支持以下值:
"lazy"
(默认值):为模块单独分割出一个 chunk 文件,可以延迟加载;"lazy-once"
:一般用于动态引入的场合,生成一份可以 “一次性满足所有导入” 的 chunk,可以延迟加载,你可以理解为组合所有导出仅生成一份文件,举个例子:做国际化多语言支持,每个国家的语言都是一份 JS 或者 JSON 配置,通过动态import()
来导入语言文件,如果这些语言文件使用"lazy-once"
来导入的话,那么切换语言时候可以秒切不需要等加载;"eager"
:避免被导入的模块生成额外的 chunk,也就是说代码分割是无效的,注意import()
依然是一个Promise
;"weak"
:使用 “弱依赖” 模式,一般用于 SSR + 代码分割的场景。此import()
方法仍然返回一个Promise
,但只会在当前已加载了此模块时,才会resolve
此模块,否则此Promise
直接reject
而并不去加载 JS 文件。一般来说,这个注释可以避免代码在 SSR 场景下,代码在服务端和客户端各运行一次而引发问题。
webpackPrefetch
/* webpackPrefetch: true */
和 /* webpackPreload: true */
:这两个注释分别表示导入的模块需要被 “预获取(prefetch)” 和 “预加载(preload)”。用好它们,可以优化网站的用户体验。
如果网站进行了代码分割,把代码分为了不同的 chunk,那么用户浏览网站时加载其他 chunk 代码便需要下载资源,影响体验。开发者可以令浏览器默默地提前加载好资源,这样后续就不需要等待资源的下载了。
它们的原理是:
/* webpackPrefetch: true */
:当前模块如果引入了某个带有此注释的模块,那么 Webpack 注入的代码会在当前模块完成加载后放置一个<link rel="prefetch" as="script" href="模块文件名.js">
标签在<head>
里,浏览器会去尝试在空闲时间发送请求预获取这些资源,并在短时间内缓存;/* webpackPreload: true */
:当前模块如果引入了某个带有此注释的模块,那么 Webpack 会在当前模块再之前的模块中注入代码,用于在当前模块被加载时放置一个<link rel="preload" as="script" href="模块文件名.js">
标签在<head>
里,浏览器会去尝试尽快请求这些资源,优先级比 prefetch 更高;这里需要注意,preload 是在当前页面刚开始加载时便已经放置了<link>
标签(也就 Webpack 早在当前页面之前更早的页面中便已经注入了代码),如果前一级的模块一开始就是同步加载的(例如是主包),那么 preload 不会生效。
一般来说,使用 prefetch 时负担会更小一点,preload 会带来较大的网络开销,一般只用在必定会用到的资源上面。
webpackIgnore
/* webpackIgnore: true */
:导入时禁用导入解析,注意此时也不会运作代码分割,一般用于引入静态部署的 JS 文件。
禁用导入解析意味着被导入的文件不会被一起打包,也不会被工具压缩代码和重命名文件。举例:
import(/* webpackIgnore: true */ './lib/mylib')
这样运行,浏览器会直接尝试加载 http://localhost:3000/static/js/lib/mylib
这个文件,既不会给 url 添加 .js
后缀,也不会加上哈希值,这个文件同样也没有被打包。
webpackExports
/* webpackExports: ["需要导入的变量或方法名"] */
:这个需要 webpack@5
版本才能用,注意只有在打包构建时才能产生效果,如果一个模块导出了很多个方法和变量,而我们只需要用到其中几个,那么可以用这个来指定打包时只保留哪些方法和变量。
以上注释都可以组合使用,使用逗号 ,
分隔即可,例如 /* webpackMode: "lazy-once", webpackChunkName: "app-utils" */
。
ESLint 注释
ESLint 的注释也是比较常见的,它们主要用于令 ESLint 跳过某一行代码的检查。可以参考 官方文档。
例如预期某一行代码不符合 ESLint 规则,但是只能这么写,或者是这么写不会引起问题,那么我们可以写一行:
// eslint-disable-next-line 要忽略的规则名
这样下面一行代码即使不符合这个规则,也不会标红。
这里列出常用的 ESLint 注释格式:
/* eslint-disable */
:在当前文件中,从下一行开始,跳过规则检查,注意这个注释不支持写成 //
双斜杠格式;
/* eslint-enable */
:在当前文件中,从下一行开始,开启规则检查,注意这个注释不支持写成 //
双斜杠格式。
上面这两个注释,还支持指定特定的规则,例如:/* eslint-disable no-console */
表示在当前文件中,从下一行开始,跳过 no-console
这条规则检查;也可以指定多个规则,用逗号分隔。
注意这两个注释不支持写成 //
双斜杠格式。
// eslint-disable-line
、// eslint-disable-next-line
:跳过当前行、下一行的规则检查,支持 /* */
格式。
这个注释支持指定特定的规则,例如:// eslint-disable-next-line no-console
表示在下一行跳过 no-console
这条规则检查;也可以同时指定多个规则,用逗号分隔。
/* eslint eqeqeq: 0 */
:在当前文件中覆写特定的 ESLint 规则配置,这里的举例就是把 eqeqeq
规则配置覆写为 0
。
规则可以使用数字、"off"
甚至数组等格式,也可以一次覆写多个规则,用逗号分隔;注意这个注释不支持写成 //
双斜杠格式。
注意:ESLint 的注释都必须以 eslint
字符串开头,否则会识别不到;有的注释不支持双斜杠 //
形式;在 ESLint 注释后面接上空格和两个连续的横杠 --
,便可以写自己的注释了,两个横杠后面的内容会被 ESLint 忽略。