前端需要理解的工程化知识

1 Git

1.1 Git 常见工作流程

Git 有4个区域:工作区(workspace)、index(暂存区)、repository(本地仓库)和remote(远程仓库),而工作区就是指对文件发生更改的地方,更改通过git add 存入暂存区,然后由git commit 提交至本地仓库,最后push推送到远程仓库。

工作目录包括.git 子目录(包含仓库所有相关的 Git 修订历史记录)、工作树,或签出的项目文件的副本。

裸存储库 只包含版本控制信息而没有工作文件(工作树)而且****.git 子目录中的所有内容存在于主目录****

完成提交 ****(commit)****前,称为"staging area"或"index"。此区域可对其进行格式化和审查。git 会在提交之前触发 pre-commit hook,使用pre-commit hook相关的脚行对提交到存储库中的更改执行完整性检查等,不满足可以阻止提交操作。

常见工作场景:

  1. 初始化本地仓库:

  2. 创建本地分支并推送关联远程:

  3. 查看分支:

  4. 合并分支:

  5. 删除分支:

  6. 标签管理:

  7. push变更代码的流程:

  8. 开发新feature功能:新建一个feature分支,并关联远程分支,然后合并到开发分支(如果存在),确定没问题再MR合并到master分支。

  9. bug修复:新建一个bugfix分支,并关联远程分支,然后合并到开发分支(如果存在),确定没问题再MR合并到master分支;

git stash:将****工作目录(修改后的跟踪文件和暂存的更改)****保存在一堆未完成的更改中并且可以随时重新应用这些更改。

1.2 git pull 和 git fetch

git pull :从中央存储库 中提取特定分支的新更改或提交 ,并更新本地存储库 中的目标分支

git fetch :相同的目的,但工作方式略不同。 git fetch 从所需的分支中提取所有新提交存储在本地存储库中的新分支 中。若要在目标分支中反映这些更改,必须在 git fetch 之后执行git merge。只有在对目标分支和获取的分支进行合并后才会更新目标分支。

git pull = git fetch + git merge

1.4 Git 和 SVN

Git是分布式版本控制工具,可本地系统克隆远端存储库,支持离线提交,基于C语言,push/pull很快,通过commit实现共享。不依赖于中央服务器来存储项目文件的所有版本。

SVN是集中版本控制工具,存储在服务器,仅支持在线提交,push/pull较慢,不支持共享。

1.5 git merge 和 git rebase

两者都是用于分支合并,关键在 commit 记录的处理上不同:

·git merge 会新建一个新的 commit 对象,然后两个分支以前的 commit 记录都指向这个新 commit 记录。这种方法会保留之前每个分支的 commit 历史。

·git rebase 会先找到两个分支的第一个共同的 commit 祖先记录,然后将提取当前分支这之后的所有 commit 记录,然后将这个 commit 记录添加到目标分支的最新提交后面。经过这个合并后,两个分支合并后的 commit 记录就变为了线性的记录了。而且git rebase 后git push需要添加 --force 进行强制推送。

1.6 git config

core.ignorecase:是否忽略大小写,默认情况下是false,除了git-clone和git-init会在创建仓库时探测并适当地设置core.ignoreCase为true。如果为true,这个选项可以启用各种变通方法,使Git在不区分大小写的文件系统(如FAT)上更好地工作。

2 webpack

Webpack核心能力:1. 打包压缩(js混淆防止源码被利用),减少网络传输;2. 文件指纹,合理利用http缓存;3. 开发服务器,热更新,改善开发体验。

Webpack的好处:1. 可以使用任意模块化标准,无须担心兼容性问题。2. 可以将非JS视为模块,使得对css、图片等资源进行更细粒度的控制。3. 前端开发也能使用npm,wepack不会运行源代码,而是作为依赖,最终合并打包。4. 非常适合开发单页应用。

webpack会非常暴力的将public目录中除页面模板(index.html)的文件复制到打包结果中。

除JS和css模块外,其他模块被视为资源模块。webpack是无法识别JS中直接书写的路径字符串的,只有通过模块化的方式导入资源,资源才会被视为模块,webpack才能将该资源的原始路径转换为打包结果的真实路径。

2.1 核心概念

2.1.1 bundle,chunk,module

bundle:是由webpack打包出来的可在浏览器直接运行的⽂件;chunk是无法在打包结果中看到的,打包结果中看到的是bundle。

chunk:代码块,⼀个chunk由多个模块组合⽽成,⽤于代码的合并和分割;简单来说,它表示通过某个入口模块找到的所有依赖的统称。每个chunk都至少有两个属性:

  1. name:默认是main;
  2. id:开发环境和name相同,生产环境是一个数字,从0开始。

module:是开发中的单个模块,在webpack的世界,⼀切皆模块,⼀个模块对应⼀个⽂件,webpack会从配置的 entry中递归开始找出所有依赖的模块。

module 就是没有被编译之前的代码,通过 webpack 的根据文件引用关系生成 chunk 文件,webpack 处理好 chunk 文件后,生成运行在浏览器中的代码 bundle。

2.1.2 构建流程

Webpack 的运⾏(编译、构建或打包)流程是⼀个串⾏的过程,从启动到结束会依次执⾏以下流程:

  1. 初始化参数:从配置⽂件和 Shell 语句(CLI)中读取与合并参数并与默认配置融合,得出最终的参数;依托第三方库yargs完成。
  2. 开始编译:⽤上⼀步得到的参数初始化 Compiler 对象,加载所有配置的插件,执⾏对象的 run ⽅法开始执⾏编译;
  3. 确定⼊⼝:根据配置中的 entry 找出所有的⼊⼝⽂件;
  4. 编译 模块:从⼊⼝⽂件出发,调⽤所有配置的 Loader 对模块进⾏翻译 (语法分析转换为AST) ,再找出该模块依赖的模块 进行记录 ,再递归本步骤直到所有⼊⼝依赖的⽂件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使⽤ Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据⼊⼝和模块之间的依赖关系,组装成⼀个个包含多个模块的 Chunk,再把每个 Chunk 转换成⼀个单独的⽂件加⼊到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和⽂件名,把⽂件内容写⼊到⽂件系统。

在以上过程中,Webpack 会在特定的时间点⼴播出特定的事件,插件在监听到感兴趣的事件后会执⾏特定的逻辑,并且插件可以调⽤ Webpack 提供的 API 改变 Webpack 的运⾏结果。

****target配置选项是构建目标,****指定构建出对应运行环境的代码。默认值为 "browserslist",如果没有找到 browserslist 的配置,则默认为 "web"。虽然 webpack 不支持 向 target 属性传入多个字符串,但是可以通过设置两个独立配置(module.exports = [serverConfig, clientConfig];),来对 library 进行多套构建。

2.1.3 mode 模式

提供 mode 配置选项,告知 webpack 使用相应模式的内置优化。Webpack4+支持,取值有 'none' | 'development' | 'production',默认值是 'production'。

(1)development

  1. 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development。
  2. 启用 NamedChunksPlugin 和 NamedModulesPlugin 为所有的module(源文件)和chunk(构建输出的文件)定义一个名字。方便于浏览器调试;可以快速地对增加的内容进行编译;提供了更精确、更有用的运行时错误提示机制。

(2)production

  1. 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。
  2. 启用插件(最后一个为非内置插件):
    • FlagDependencyUsagePlugin:检测并标记模块之间的从属关系
    • FlagIncludeChunksPlugin:可以让 Webpack 根据模块间的关系依赖图中,将所有的模块连接成一个模块
    • ModuleConcatenationPlugin:告诉 Webapck 去清除一个大的模块文件中的未使用的代码,这个大的文件模块可以是自定义的,也可以是第三方的(注意:一定要 package.json 文件中添加 "sideEffects": false)
    • NoEmitOnErrorsPlugin
    • OccurrenceOrderPlugin
    • SideEffectsFlagPlugin:告知 Webapck 各个模块间的先后顺序,这样可以实现最优的构建输出
    • TerserPlugin:替代 uglifyjs-webpack-plugin 插件。它的作用依然是对构建输出的代码进行压缩

(3)none: 不使用任何默认优化选项 ,即启动 Webpack 打包时关闭默认的内置插件。

2.1.4 entry 入口

入口起点(entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部依赖图(dependency graph) 的 开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。动态加载的模块不是入口起点。

Webpack 寻找相对路径的文件时(解析入口点(entry point)和加载器(loader))会以 context 配置选项 为根目录,context 配置选项必须是绝对路径字符串。默认使用 Node.js 进程的当前工作目录,但是推荐在配置中传入一个值,使得配置独立于 CWD(current working directory, 当前工作目录)。

Webpack提供entry 配置选项,来指定一个(或多个)不同的入口起点,默认值是 ./src/index.js。

单入口语法:string | string[],是多入口对象语法中只有一个main-chunk的简写。entry 设置为string[],在作为库的时候即和output.library一起使用时只有数组中最后一个模块会暴露。

Webpack 配置的可扩展是指配置可重用,并且可以与其他配置组合使用,使得可以将关注点从环境(environment)、构建目标(build target)、运行时(runtime)中分离。然后使用专门的工具(如 webpack-merge)将它们合并起来。

多入口对象语法 :entry: { <entryChunkName> string | [string] } | {},entry 是一个 object,对象中每个属性对应一个Chunk,属性名key即chunk名,属性值value可以是string | string[] | {},描述每个入口的对象有 如下属性

  1. dependOn:当前入口所依赖的入口,它们必须在该入口被加载前被加载。使用 dependOn 选项可以与另一个入口 chunk 共享模块(相当于将依赖的入口从当前入口剥离)。
  2. filename:指定要输出的文件名称。
  3. import:启动时需加载的模块。
  4. library:指定 library 选项,为当前 entry 构建一个 library。
  5. runtime:运行时 chunk 的名字。如果设置了,就会创建一个新的运行时 chunk。
  6. publicPath:当该入口的输出文件在浏览器中被引用时,为它们指定一个公共 URL 地址。

确保runtime 和 dependOn 不应在同一个入口上同时使用,确保dependOn 不能是循环引用的,确保 runtime 不能指向已存在的入口名称,否则均会抛出错误。

分离 app(应用程序) 和 vendor(第三方库) 入口的场景 ,webpack4开始,不推荐为 vendor 或其他不是执行起点创建 entry,而是应该使用 optimization.splitChunks 选项,将 vendor 和 app模块分开,并为其创建一个单独的文件。在多页面应用程序中,server会拉取一个新的 HTML 文档到客户端。页面重新加载此新文档,并且资源被重新下载。借助optimization.splitChunks 为页面间共享的应用程序代码创建 bundle,并对多个入口起点之间的大量代码/模块进行复用。根据经验,每个 HTML 文档只使用一个入口起点。

动态入口语法:entry设置为一个函数,那么它将会在每次 make 事件中被调用。make 事件在 Webpack 启动和每当监听文件变化时都会触发。动态入口使得可以从外部来源(远程服务器,文件系统内容或者数据库)获取真正的入口。

2.1.5 output 输出

output 配置选项指定webpack 对「bundle、asset 和其他所打包或使用 webpack 载入的任何内容」如何进行输出和输出位置。只能指定一个 output 配置。主要输出文件的默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。

在 webpack 配置中,output 配置选项的最低要求是,包括output.filename的一个对象。

output.filename

如果webpack配置会生成多于一个 "chunk"(例如,使用多个入口起点或使用 CommonsChunkPlugin 插件),则在output.filename上应该使用占位符(substitutions) 来确保每个文件具有唯一的名称。占位符可以是以下一个或多个组合:

  1. [name]:chunk 名称。
  2. [id]:内部chunk id,从0自增。
  3. [fullhash]:整个编译过程(compilation)的 hash,webpack4是[hash](已弃用)。
  4. [chunkhash]:每一个入口(entry)的hash。
  5. [contnethash]:每一个模块的hash。

因为 ExtractTextWebpackPlugin 提取出来的内容是代码内容本身,而不是由一组模块组成的 Chunk,所以ExtractTextWebpackPlugin 插件使用 contenthash 而不是 chunkhash 来代表哈希值。

hash的长度可以使用 [chunkhash:16](默认为 20)来指定,或者通过指定 output.hashDigestLength 在全局配置长度

也可以使用函数:

output.pathinfo

类型是boolean | 'verbose',用于告知 webpack 在 bundle 中引入「所包含模块信息」的相关注释。此选项在 development 模式时的默认值为 true,而在 production 模式时的默认值为 false。当值为 'verbose' 时,会显示更多信息,如 export以及运行时依赖。生产环境(production)下,不应该使用true 或 'verbose'。

output.publicPath

用于指定在浏览器中所引用的「此输出目录对应的公开 URL」,相对 URL(relative URL) 会被相对于 HTML 页面(或 <base> 标签)解析。相对于服务的 URL(Server-relative URL)、相对于协议的 URL(protocol-relative URL) 或绝对 URL(absolute URL) 也可能用到,比如托管在CDN的资源。

该选项的值是以 runtime(运行时) 或 loader(载入时) 所创建的每个 URL 的前缀。因此大多数情况,该选项的值以 / 结尾。

对于按需加载(on-demand-load)或加载外部资源(external resources)(如图片、文件等)来说,output.publicPath 是很重要的选项。如果指定错误的值,会导致404错误。

output.chunkFilename

只用于配置非初始(non-initial)chunk 文件,即在运行时生成的chunk的名称。取值和output.filename类似。常见场景包括:

  1. 使用 CommonsChunkPlugin,用于创建一个单独的文件中,由多个入口点之间共享的公共模块。不过从 Webpack v4+ 开始,CommonsChunkPlugin 已被删除,转而使用 optimization.splitChunks。
  2. 使用 import('path/to/module') 动态加载的模块等。
output.crossOriginLoading

按需加载是通过JSONP实现的。JSONP 的原理是动态地向 HTML 中插入一个 <script>标签去加载异步资源。该选项配置启用 cross-origin 属性按需加载 chunk,仅在 target 设置为 'web' 时生效。<script>标签的crossOrigin值:

·'anonymous' - 不带凭据(credential) 启用跨域加载

·'use-credentials' - 携带凭据(credential) 启用跨域加载

默认情况下(即未指定 crossOrigin 属性时),CORS(跨域资源共享) 根本不会使用。而且script标签的可以使用cross-origin 属性来使那些将静态资源放在另外一个域名的站点打印详细错误信息。

output.library

当用 Webpack 去构建一个可以被其他模块导入使用的库时使用。类型是string | string[]| {},对象语法包括以下属性:

  1. output.library.name指定库的名称。
  2. output.library.type配置将库暴露的方式,以何种方式将入口点的返回值赋值给output.library.name提供的名称,之前是使用的配置选项是output.libraryTarget(不推荐,未来可能会废弃)。取值有:
    1. 'var'(默认),入口起点的返回值 将会被赋值给一个 output.library.name名称的变量。
    2. 'this',入口起点的返回值将会被赋值给 this 对象下的 output.library.name 属性。
    3. 'global',入口起点的返回值将会被复制给全局对象下的 output.library.name
    4. 'commonjs',入口起点的返回值将使用 output.library.name 赋值给 exports 对象。
    5. 'commonjs2',入口起点的返回值将会被赋值给 module.exports。
    6. 'amd',将库暴露为 AMD 模块。
    7. 'module',输出ES模块。
    8. 'window',入口起点的返回值将会被赋值给 window 对象下的 output.library.name
  3. output.library.export,类型string | string[],指定入口起点返回值的哪一个子模块应该被暴露为一个库。默认为 undefined,将会导出整个(命名空间)对象。string[]将被解析为一个要分配给库名的模块的路径。

2.1.6 resolve 解析

resolve配置选项用于设置模块如何被解析。类型是object。有以下几个属性:

  1. resolve.alias,类型object,创建 import 或 require 的别名。
  2. resolve.mainFields,此选项将决定当从 npm 包中导入模块时,在 package.json 中使用哪个字段导入模块,默认值受target的影响。当 target 属性设置为 webworker, web 或者没有指定时,resolve.mainFields的值是['browser', 'module', 'main'],即会优先从package.json 的 browser 属性解析文件。对于其他任意的 target(包括 node),默认值为['module', 'main'],即会优先选择package.json 的的module属性。
  3. resolve.extensions,类型string[],尝试按顺序解析这些后缀名。当导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试访问文件是否存在,支持使用 '...' 访问webpack设置的默认拓展名。

2.1.7 module(包括loader)

moudlue配置选项决定了如何处理项目中的不同类型的模块。Webpack 天生支持如下模块类型有 ECMAScript 模块, CommonJS 模块, AMD 模块, Assets, WebAssembly 模块。

module.rules

用于配置模块的读取和解析规则,通常用来配置 Loader。每个Rule(规则)可以分为三部分 - 条件(condition),结果(result)和嵌套规则(nested rule)。

条件匹配:通过 Rule.test(引入所有通过断言测试的模块)、Rule.include(引入符合任何条件的模块)、Rule.exclude (排除所有符合条件的模块)三个配置项来选中 Loader 要应用规则的文件。其中任意一个提供,就不能再提供 Rule.resource。支持字符串,数组和正则。

应用规则:对选中的文件通过 Rule.use 配置项来应用 Loader,传递字符串(如:use: [ 'style-loader' ])是 loader 属性的简写方式(如:use: [ { loader: 'style-loader'} ])。可以分别向 Loader 在Rule.use.options上传入参数。Rule.enforce选项设置loader的种类,没有值表示是普通(normal) loader,pre表示前置loader(相当于放到数组最后面),post 表示后置loader(相当于放到数组最前面)。多个Loader默认按照从右往左的顺序应用,因为webpack选择了compose这样的函数式编程方式,这种方式的表达式执行是从右向左的。而所有一个接一个地进入的 loader,都有两个阶段:

  1. Pitching 阶段:loader 上的 pitch 方法,从左往右的顺序调用,而且如果一个patching方法返回了结果,就会跳过其他的loader直接进入Normal阶段。
  2. Normal 阶段::loader 上的常规方法从右往左的顺序调用。模块源码的转换, 发生在这个阶段。
  3. resolve.modules,告诉 webpack 解析模块时应该搜索的目录。默认值是['node_modules'],可以在之前添加目录,会优先于 node_modules/ 搜索。
  4. resolve.descriptionFiles,用于描述第三方模块的 JSON 文件,默认值是['package.json']
  5. resolve.enforceExtension,默认值是false,如果是 true,导入语句将不允许无扩展名文件。Webpack 4+ 版本,只针对node_modules中模块的resolve.moduleExtensions 和 resolve.enforceModuleExtension已经被删除。

resolver(解析器) 是一个帮助 webpack 从每个 require/import 语句中,找到需要引入到 bundle 中的模块代码的库。 当打包模块时,webpack 使用 enhanced-resolve 来解析三种文件路径:

  1. 绝对路径:直接使用,不用解析。
  2. 相对路径:在 import/require 中给定的相对路径,会拼接使用 import 或 require 的资源文件所处的目录(上下文目录),来生成模块的绝对路径。
  3. 模块路径 :在 resolve.modules 中指定的所有目录中检索模块。 可以通过 resolve.alias配置别名的方式来简化模块路径的首个目录。

一旦根据上述规则解析路径后,resolver 将会检查路径是指向文件还是文件夹:

  1. 如果路径指向文件,如果文件具有扩展名,则直接将文件打包。 否则,将使用 resolve.extensions 选项作为文件扩展名来解析。
  2. 如果路径指向一个文件夹 ,则进行如下步骤寻找具有正确扩展名的文件:
    1. 如果文件夹中包含 package.json 文件,则会根据 resolve.mainFields 配置中的字段顺序查找,并根据 package.json 中的符合配置要求的第一个字段来确定文件路径。
    2. 如果不存在 package.json 文件或 resolve.mainFields 没有返回有效路径,则会根据 resolve.mainFiles 配置选项中指定的文件名顺序查找,看是否能在 import/require 的目录下匹配到一个存在的文件名。
    3. 然后使用 resolve.extensions 选项,以类似的方式解析文件扩展名。

resolveLoader 配置选项,与resolve配置选项的value对象的属性集相同,但仅用于解析 webpack 的 loader 包。

如何编写一个loader?

loader 本质上是导出为函数的 JavaScript 模块。loader runner 会调用此函数,然后将上一个 loader 产生的结果或者资源文件传入进去。函数中的 this 作为上下文可以调用 loader runner 中的一些实用的方法,比如可以使 loader 调用方式变为异步。编写 loader 时应该遵循以下准则:

  1. 简单:loaders 应该只做单一任务。这不仅使每个 loader 易维护,也可以在更多场景链式调用。
  2. 链式:利用 loader 可以链式调用的优势,写多个功能隔离的loader。loader 可以被链式调用意味着不一定要输出 JavaScript。只要下一个 loader 可以处理这个输出,这个 loader 就可以返回任意类型的模块。
  3. 模块化:保证输出模块化。
  4. 无状态:确保 loader 在不同模块转换之间不保存状态。
  5. loader 工具库:充分利用 loader-utils 包。

可以用 ​webpack-defaults​ 来生成开始编写 loader 必要的样板代码(boilerplate code)。

如果是单个处理结果,可以在同步模式 中直接return返回。如果有多个处理结果,则必须调用 this.callback(),且不能有return。在异步模式中,必须调用 this.async() 来告知 loader runner 等待异步结果,它会返回 this.callback() 回调函数。随后 loader 必须返回 undefined 并且调用该回调函数。

2.1.8 Plugin

相比loader 是对某些类型的模块进行转换,插件可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量等等。

webpack 插件是一个具有​ apply 方 ​法的JavaScript Class 对象。apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象。插件能够 hook 到编译(compilation)中发出的每一个关键事件中。

由于插件可以携带参数/选项,因此必须在 webpack 配置中,向 plugins 属性传入一个插件的 new 实例,插件会按从左到右的顺序执行。

所有webpack内置插件都作为webpack的静态属性存在的,使用则是通过new webpack.PluginName(options)。

SplitChunksPlugin

最初,块(Chunks)及其内部导入模块是通过 Webpack 内部的依赖关系图的父子关系连接起来的,用于提取公共模块。从 Webpack v4+ 开始,CommonsChunkPlugin 已被删除,转而使用 optimization.splitChunks选项配置。默认情况下 Webpack 会按照下列条件自动分割 Chunks:

  1. 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹。
  2. 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积)。
  3. 当按需加载 chunks 时,并行请求的最大数量小于或等于 30。
  4. 当加载初始化页面时,并发请求的最大数量小于或等于 30。

当 Webpack 处理文件路径时,它们在 Unix 系统上始终包含 /,在 Windows 上始终包含 \。因此,必须在 cacheGroup.test 字段中使用 [\\/] 来表示路径分隔符。在跨平台使用时,cacheGroups.test 中的 / 或 \ 将导致问题。

DefinePlugin

DefinePlugin 允许在编译时将代码中的变量替换为其他值或表达式。这在需要根据开发模式与生产模式进行不同的操作时,非常有用。传递给 DefinePlugin 的每个键都是一个标识符或多个以 . 连接的标识符。

  1. 如果该值为字符串,它将被作为代码片段来使用。
  2. 如果该值不是字符串,则将被转换成字符串(包括函数方法)。
  3. 如果值是一个对象,则它所有的键将使用相同方法定义。
  4. 如果键添加 typeof 作为前缀,它会被定义为 typeof 调用。
DllPlugin

DllPlugin 和 DllReferencePlugin 主要功能是可以将可共享且不经常改变的代码,抽取成一个共享的库,避免进行二次构建,同时也对构建时间进行优化。

通常来说,我们的代码都可以至少简单区分成业务代码和第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到 dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码。

DllReferencePlugin 和 DllPlugin 都是在 单独的 webpack 配置中使用的,用于创建一个 dll-only-bundle。建议 DllPlugin 只在 entryOnly: true 时使用,否则 DLL 中的 tree shaking 将无法工作,因为所有 exports 均可使用。

插件的作用是:

  1. 分离代码:业务代码和第三方模块可以被单独打包到不同的文件中
    1. 避免打包出单个文件的大小太大,不利于调试
    2. 将单个大文件拆分成多个小文件之后,一定情况下有利于加载(不超过浏览器一次性请求的文件数的情况下,并行下载肯定比串行快)
  2. 提升构建速度:第三方库代码没有变更时,由于我们只构建业务相关代码,相比全部重新构建自然要快得多

首先, 使用 DllPlugin 打包需要分离到动态库的模块

然后在主构建配置文件webpack.config.js 中使用动态库文件,要用到 DllReferencePlugin,这个插件通过引用 dll 的 manifest 文件来把依赖的名称映射到模块的 id 上,之后再在需要的时候通过内置的 webpack_require 函数来 require 它们:

DllPlugin 中的 name 参数必须和 output.library 中保持一致,因为DllReferencePlugin 会去 manifest.json 文件读取 name 字段的值,并作为在全局变量中获取动态链接库中内容时的全局变量名。

最后在在入口文件引入 dll 文件,生成的 dll 暴露出的是全局函数,因此还需要在入口文件中引入对应的 dll 文件。(也可以使用 AddAssetHtmlPlugin,将打包好的 DLL 库引入到 HTML 模版中)。

如何编写一个Plugin(插件)?

webpack 插件由以下组成:

  1. 一个 JavaScript 命名函数或 JavaScript 类。
  2. 在插件函数的 prototype 上定义一个 apply 方法(也就是Class里的方法)。
  3. 指定一个绑定到 webpack 自身的事件钩子。
  4. 处理 webpack 内部实例的特定数据。
  5. 功能完成后调用 webpack 提供的回调。

可以使用 schema-utils 来校验在webpack配置中传入插件的options,这点和loader类似。

当hook入到的生命周期钩子函数是同步的,比如compile(beforeCompile 之后立即调用,但在一个新的 compilation 创建之前) 阶段,只有同步的 tap 方法可以使用。当hook入到的生命周期钩子函数是异步的,比如run 阶段或emit(输出 asset 到 output 目录之前执行)阶段,则可以使用 tapAsync 、 tapPromise以及tap方法。

当用 tapAsync 方法来绑定插件时,必须在最后调用指定的回调函数的最后一个参数 callback 。当我们用 tapPromise 方法来绑定插件时,必须返回一个 pormise,异步任务完成后 resolve,即可以使用async/await

有用的 plugin

clean-webpack-plugin:重新构建成功之前webpack的output.path 目录中的所有文件将被删除一次,但目录本身不会被删除。

html-webpack-plugin:自动化的生成HTML文件,这对于文件名中包含每次编译都会更改的hash的 webpack bundle特别有用。如果是多入口,则对应的script标签都会包括在生成的html文件中。如果在 webpack 的输出中有任何 CSS 资源(例如,使用 mini-css-extract-plugin 提取的 CSS),那么这些资源将包含在 HTML 头部的 <link> 标签中。也支持通过template选项来提供自定义模板html。通过filename选项自定义生成的html文件名。通过chunks选项自定义生成的html文件要引入哪些script标签。注意,每个HtmlWebpackPlugin实例只生成一个html,可以通过配置多个HtmlWebpackPlugin实例生成多个html。public/index.html 文件是一个会被 html-webpack-plugin 处理的模板。在构建过程中,资源链接会被自动注入。

copy-webpack-plugin:将已存在而不是构建产生的单个文件或整个目录(from)复制到构建目录(to)。如果希望 webpack-dev-server 在开发期间将文件写入输出目录,可以使用 writeToDisk 选项或 write-file-webpack-plugin 强制它。

2.2 sourceMap

SourceMap 就是一个信息文件,里面储存着代码的位置信息。这种文件主要用于开发调试,现在代码都会经过压缩混淆,这样报错提示会很难定位代码。通过 SourceMap 能快速定位到源代码,并进行调试。通常情况 SourceMap 在开发环境开启,线上环境关闭。

使用场景

  1. 开发期间,开发人员能直接通过浏览器调试工具直接定位错误或进行 Debug 调试。
  2. 线上排查问题的时候可以将 SourceMap 上传到错误监控系统。

首先,浏览器一般默认开启SourceMap 调试,使得控制台的 Console 面板的错误提示,直接点击就会跳转到对应的源文件出错位置:

如果没 Console 没报错,但是页面显示不正确。可以点击控制台的 Sources (源代码)tab,源文件都在 webpack:// 目录下,或者直接搜索文件,打开源文件后进行断点调试。而对于按需加载的路由,只有页面加载了,源文件才会在该目录下显示。

生成sourceMap,浏览器控制台才能直接显示原始代码出错的位置,而不是转换后的代码,使用Webpack 的devtool字段配置 sourceMap,都是eval、source-map、inline、cheap 和 module 的自由组合或都没有(none):

  1. none:省略 devtool 选项,即不生成 source map。
  2. eval:使用 eval 包裹模块代码,并且在末尾追加注释 //@sourceURL。
  3. source-map:产生 .map 文件,每个 bundle 文件后缀加上 sourcemap 的 sourceURL 或 dataURL,包括行列信息,loader 也有对应的 sourcemap。
  4. cheap:sourcemap 中的 mappings 只有对应行信息,没有列文件,loader 也没有对应的 sourcemap,对应的都是 loader 转换后的代码,不是纯正的源代码。
  5. inline:将 .map 作为 DataURI(Base64)嵌入,不单独生成 .map 文件(不推荐使用,因为这样会造成源代码体积巨大)。
  6. module:包含 loader 的 sourcemap。

开发环境推荐:eval-cheap-module-source-map。

生产环境推荐:

  1. ****source-map,****应该将服务器配置为不允许普通用户访问 source map 文件。
  2. none
  3. ****nosources-source-map,****会暴露反编译后的文件名和结构,但它不会暴露原始代码。它可以用来映射客户端上的堆栈跟踪,而无须暴露所有的源代码,因此可以将 source map 文件部署到 web 服务器。
  4. ****hidden-source-map ,****与 source-map 相同,但不会为 bundle 添加引用注释,即浏览器不会主动去请求map文件。适合只想 source map 映射那些源自错误报告的错误堆栈跟踪信息,但不想为浏览器开发工具暴露 source map的情况,且该设置不应。

原因:

  1. 使用 cheap 模式可以大幅提高 souremap 生成的效率。大部分情况调试并不关心列信息,而且就算 sourcemap 没有列,有些浏览器引擎(例如 v8)也会给出列信息。
  2. 使用 eval 方式可大幅提高持续构建效率。
  3. 使用 module 可支持 babel 这种预编译工具。
  4. 使用 eval-source-map 模式可以减少网络请求。这种模式开启 DataUrl 本身包含完整 sourcemap 信息,并不需要像 sourceURL 那样,浏览器需要发送一个完整请求去获取 sourcemap 文件,这会略微提高点效率。而生产环境中则不宜用 eval,这样会让文件变得极大。

也可以直接使用 SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin 来替代使用 devtool 选项,可以进行更加精细化的控制。切勿同时使用 devtool 选项和 SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin 插件。devtool 选项在内部添加过这些插件,所以最终将应用两次插件。

sourceMap方便调试,但也会暴露源代码,存在以下安全问题:

  1. 代码被抄袭:查看源码,抄走对应功能。
  2. 业务流失:竞争对手拿到源码,宣扬其中的漏洞或者缺陷,或者直接写后台,成本少,价格低,抢走业务。
  3. 系统被攻击:绕过权限获得对应资源。

或者可以使用nosources-source-map创建的 source map 不包含 sourcesContent(源代码内容)。它可以用来映射客户端上的堆栈跟踪,而无须暴露所有的源代码。你可以将 source map 文件部署到 web 服务器。

2.3 文件监听(watch 和 watchOptions)

Webpack 可以监听文件变化,当它们修改后会重新编译。watch默认值是false,即不开启监听。

2.4 模块热替换(HMR)

模块热替换( HMR-Hot Module Replacement),是指在应用程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面。主要通过以下几种方式,来显著加快开发速度:

  1. 保留在完全重新加载页面期间丢失的应用程序状态
  2. 只更新变更内容,以节省宝贵的开发时间
  3. 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。

页面的刷新一般分为两种

  1. 一种是页面刷新,直接 window.location.reload(),不保留页面状态,比如Live Server。
  2. 基于 WDS (webpack-dev-server,对应webpack命令为webpack server) 的模块热替换(HMR),只需要局部刷新页面上发生变化的模块,同时可以保留当前的页面状态。

实现原理:首先是建立起浏览器端和服务器端之间的通信,浏览器会接收服务器端推送的消息,如果需要热更新,浏览器发起 HTTP 请求去服务器端获取打包好的资源解析并局部刷新页面(橙色框是浏览器端,红色框是服务端;绿色方框是 Webpack 代码控制的区域,蓝色方框是 webpack-dev-server 代码控制的区域)。

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包 ,并将打包后的代码通过JavaScript 对象保存在内存中。
  2. 第二步, webpack 和 webpack-dev-server 进行接口交互,主要是 webpack-dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API 对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. 第三步是 webpack-dev-server 对文件变化的一个监控,与第一步监控代码变化重新打包不同:当配置devServer.watchContentBase为true时,Server会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行live reload(浏览器刷新)。
  4. 第四步是webpack-dev-server 通过 sockjs在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息(新模块的 Hash 值或第三步中 web-dev-server 监听静态文件变化的信息)告知webpack-dev-server/client浏览器端,
  5. 第五步,webpack-dev-server/client 端并不能够请求更新的代码,也不会执⾏模块热替换的操作,而是交给webpack/hot/dev-server,它的⼯作就是根据 webpack-dev-server/client 传给它的信息以及 webpack-dev-server 的配置决定是刷新浏览器还是进⾏模块热替换。
  6. 如果是模块热替换,由HotModuleReplacement.runtime (客户端 HMR 的中枢)将接收到webpack/hot/dev-server传递的新模块的 hash 值,并通过JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回⼀个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码(图中7、8、9步)。
  7. 第 10 步,HotModulePlugin 将会对新旧模块进⾏对⽐,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引⽤。是决定 HMR 成功与否的关键步骤
  8. 若 HMR 失败,则回退到live reload (刷新浏览器 )获取最新打包代码。

2.5 webpack 相关的优化

2.5.1 ⽤webpack来优化前端性能

⽤ webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。

  1. 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 terser-webpack-plugin(默认使用 terser 来压缩 JavaScript) 来压缩JS⽂件, 利⽤ CssMinimizerWebpackPlugin (使用 cssnano 优化和压缩 CSS)来压缩css ,利用HtmlWebpackPlugin来压缩HTML。
  2. 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径。
  3. Tree Shaking(通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)):将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现。
  4. Code Splitting(代码分割): 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存。
    1. 入口起点:使用 entry 配置手动地分离代码。
    2. 防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk。
    3. 动态导入:通过模块的内联函数(import())调用来分离代码。
  5. 提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码。

2.5.2 提⾼webpack的构建和打包速度

  1. thread-loader:并⾏编译loader,利⽤缓存来使得 rebuild 更快。

  2. 外部扩展(externals): 将不怎么需要更新的第三⽅库脱离webpack打包,不被打⼊bundle中,从⽽减少打包时间,⽐如生产环境下,jQuery、vue、vuex、vue-router、axios等⽤script标签cdn引⼊。

  3. dll: 采⽤webpack的 DllPlugin 和 DllReferencePlugin 引⼊dll,让⼀些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间 。

4.利⽤缓存: webpack.cache 、babel-loader.cacheDirectory、thread-loader.cache 都可以利⽤缓存提⾼ rebuild效率缩⼩⽂件搜索范围。

5.多⼊⼝情况下,使⽤ SplitChunksPlugin 来提取公共代码

  1. 使⽤ Tree-shaking 和 Scope Hoisting 来剔除多余代码

  2. 使用 esbuild-loader来提升压缩速度。

  3. 使用现代模式构建应用,为现代浏览器交付原生支持的 ES2015 代码,并生成一个兼容老浏览器的包用来自动回退。比如Vue CLI 中,vue-cli-service build命令添加 --modern 选项。Vue CLI 会产生两个应用的版本:一个现代版的包,面向支持 ES modules 的现代浏览器,另一个旧版的包,面向不支持的旧浏览器,仅生成一个HTML来智能的判断应该加载哪个包:

(1)现代版的包会通过 <script type="module"> 在被支持的浏览器中加载;它们还会使用 <link rel="modulepreload"> 进行预加载。

(2)旧版的包会通过 <script nomodule> 加载,并会被支持 ES modules 的浏览器忽略。

(3)一个针对 Safari 10 中 <script nomodule> 的修复会被自动注入。

2.6 其他配置

2.6.1 externals

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。

3 npm

包是模块的集合,用于解决某一方面的问题,一个第三方库可以看作一个包,每个包里面包含一个或多个模块。每个包可能有自己的github地址,官网,版本,依赖等等。包管理器(官方包管理器npm ,社区包管理器yarn/pnpm/cnpm)所要解决的问题:1. 快速的下载安装包;2. 卸载包;3. 优雅的升级包;4. 避免版本冲突。

3.1 npm CLI

npm init:创建一个package.json文件。-y/--yes跳过调查问卷。

npm install:安装一个包及其依赖的任何包。依赖项的安装按npm-shrinkwrap.json > package-lock.json > yarn.lock的优先顺序驱动。默认情况下,npm install 将任何指定的包保存到devDependencies中。-D 或 --save-dev: 包将出现在 devDependencies 中。

npm命令合集:

npx运行的时候,会到node_modules/.bin路径和环境变量PATH里面,检查命令是否存在,可以用来运行不是全局安装或者找不到的\。Bash 内置的命令不在PATH里面,所以不能用。只要 npx 后面的模块无法在本地发现,就会下载同名模块。npx 能避免全局安装模块,会将模块下载到一个临时目录,使用以后再删除。npx 还可以执行 GitHub 上面的模块源码,前提是远程代码必须是一个模块,即必须包含package.json和入口脚本。

3.2 package.json

3.2.1 必须属性

  1. name: 必须小于等于214个字符,不能以.或_开头,不能有大写字母,因为名称最终成为 URL 的一部分,因此不能包含任何非URL安全字符。 npm官方建议我们不要使用与核心 node模块相同的名称。不要在名称中加 js或 node。如果需要可以使用engines来指定运行环境。name会作为参数传递给 require,因此它应该是简短的,但也需要具有合理的描述性。

  2. version:格式为 x.x.x,name 和 version 一起构成一个标识符,该标识符被认为是完全唯一的。每次发布时 version不能与已存在的一致。

3.2.2 描述信息

  1. description:用于编写描述信息的字符串。有助于模块在 npm库被搜索发现。
  2. keywords:字符串组成的数组,有助于模块在 npm库被搜索发现。
  3. homepage:项目的主页地址。
  4. bugs:用于反馈项目问题的 issue 地址或者邮箱。
  5. author和 contributors:author和 contributors均表示当前项目的共享者。contributors是对象数组,具有 name字段和可选的 url及 email字段。author 可以是具有 name字段和可选的 url及 email字段的对象,或由name, url和email 三部分组成的字符串("edemao edemao@xx.com (https://edemao.top/)")。
  6. repository:指定一个源代码存放地址。

3.2.3 依赖配置

  1. dependencies和devDependenciesdependencies:指定项目运行所依赖的模块,devDependencies指定项目开发所需要的模块。值对象的每一项为一个键值对,前面是模块名称,后面是对应模块的版本范围。版本号遵循"major.minor.patch"的格式规定(主版本号.次版本号.修补版本号)。修补版本中的更改表示不会破坏任何内容的错误修复。次要版本的更改表示不会破坏任何内容的新功能。主要版本的更改代表了一个破坏兼容性的大变化。 如果用户不适应主要版本更改,则内容将无法正常工作。先行版本号及版本编译信息可以加到"主版本号.次版本号.修补版本号"的后面,作为延伸(1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0)。主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 API 不应该被视为稳定版。

  2. 固定版本:比如 5.3.1,安装时只安装指定版本。

  3. 波浪号:比如 ~5.3.1, 表示安装 5.3.x 的最新版本(不低于5.3.1),但是不安装5.4.x,也就是说安装时不改变大版本号和次要版本号。

  4. 插入号:比如 ˆ5.3.1, ,表示安装 5.x.x 的最新版本(不低于5.3.1),但是不安装 6.x.x,也就是说安装时不改变大版本号。需要注意的是,如果大版本号为 0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。

  5. latest:安装最新版本。

依赖安装时,--save参数表示写入dependencies,--save-dev表示写入devDependencies。

  1. peerDependencies:就是用来供插件指定其所需要的主工具的版本。比如,项目依赖 A 模块和 B 模块的 1.0.0 版本,而 A 模块本身又依赖 B 模块的 2.0.0 版本,用 peerDepedencies 指定 A 模块 使用 B 的时候,必须是 2.0.0 版本:{"name": "A","peerDependencies": {"B": "2.0.0"}}。注意,从npm 3.0版开始,初始化的时候 peerDependencies不会默认带出。

  2. bundledDependencies:指定发布的时候会被一起打包的模块。

  3. optionalDependencies:可选的项目运行依赖,写法和dependencies一样,不同之处在于如果安装失败不会导致 npm install失败。

  4. engines:指明模块运行的平台限制,比如 Node或者 npm的某个版本或者浏览器。

3.2.4 文件和目录

  1. files:是模块下文件名或者文件夹名构成的数组,如果是文件夹名,则文件夹下所有的文件也会被包含进来(除非文件被另一些配置排除了)。可以在模块根目录下创建一个.npmignore文件,写在这个文件里边的文件即便被写在files属性里边也会被排除在外,个文件的写法与.gitignore类似。

  2. browser:指定供浏览器使用的模块版本。指定浏览器打包工具比如 Browserify该打包的文件。

  3. main:指定加载的入口文件,require导入的时候会加载这个文件。默认值是模块根目录下面的index.js。

  4. man:用来指定当前模块的 man文档的位置。

  5. directories:directories制定一些方法来描述模块的结构, 用于告诉用户每个目录在么位置。

  6. bin:用来指定每个内部命令对应的可执行文件的位置。node工具必然会用到该字段。当我们编写一个cli工具的时候,需要指定工具的运行命令,比如webpack执行 bin/index.js文件中的代码:"bin": { "webpack": "bin/index.js" }。当模块以依赖的方式被安装,如果存在bin选项,会在node_modules/.bin/生成对应的文件,并建立符号链接。由于node_modules/.bin/目录会在运行时加入系统的 PATH 变量,所以 npm run 就可以不带路径,直接通过命令来调用这些脚本文件。所有 node_modules/.bin/ 目录下的命令,都可以用 npm run [命令] 的格式运行。在命令行键入npm run,按tab键会显示所有可以使用的命令。

3.2.5 脚本配置

  1. scripts:指定了运行脚本命令的 npm 命令行缩写。使用 scripts字段可以快速的执行 shell 命令,可以理解为 alias。scripts可以直接使用node_modules中安装的模块,否则需要使用npx命令才能直接运行:"scripts": { "build": "webpack"} // npm run build 相当于 npx webpack。

  2. config:用于添加命令行的环境变量。在server.js脚本就可以引用config字段的值。

也可以通过npm config set进行修改。

3.2.6 发布配置

  1. license:当前项目的协议------ 模块使用权限和限制。

  2. os:指定模块能运行的操作系统。

  3. cpu:限制模块只能在某种架构的cpu下运行。

  4. private:布尔值,可以防止一个私有模块被无意间发布,true则 npm拒绝发布它。

  5. publishConfig:在模块发布时生效,用于设置发布用到的一些值的集合。如果你不想模块被默认标记 tag 为最新的,或者默认发布到公共仓库,可以在这里配置 tag 或仓库地址。如果只想让模块被发布到一个特定的 npm仓库,通常 publishConfig会配合 private来使用。

  6. preferGlobal:布尔值,表示当用户不将该模块安装为全局模块时(即不用--global参数),true 表示显示警告。

3.2.7 第三方配置

  1. typings:用来指定TypeScript的入口文件。

  2. eslintConfig:eslint的配置可以写在单独的配置文件.eslintrc.json 中,也可以写在package.json文件的eslintConfig配置项中。

  3. babel:用来指定Babel的编译配置。

  4. unpkg:开启cdn服务。

  5. lintstage: lint-staged是一个在Git暂存文件上运行linters的工具,配置后每次修改一个文件即可给所有文件执行一次lint检查,通常配合gitHooks一起使用。

  6. gitHooks:gitHooks用来定义一个钩子,在提交(commit)之前执行ESlint检查。在执行lint命令后,会自动修复暂存区的文件。修复之后的文件并不会存储在暂存区,所以需要用git add命令将修复后的文件重新加入暂存区。在执行pre-commit命令之后,如果没有错误,就会执行git commit命令。

  7. browserslist:用来告知支持哪些浏览器及版本。Babel、Autoprefixer 和其他工具会用到它,以将所需的 polyfill 和 fallback 添加到目标浏览器。

3.3 package-lock.json

package-lock.json 是对整个依赖树进行版本固定,它准确的描述了当前项目npm包的依赖树,并且在随后的安装中会根据 package-lock.json 来安装,不考虑这个过程中是否有某个依赖有小版本的更新,从而保证是依赖树不变。

注意,使用cnpm install时候,并不会生成 package-lock.json 文件,也不会根据 package-lock.json 来安装依赖包,还是会使用 package.json 来安装。

package-lock.json 可能被意外更改的原因:

  1. package.json 文件修改了;
  2. 挪动了包的位置:将部分包的位置从 dependencies 移动到 devDependencies 这种操作,虽然包未变,但是也会影响 package-lock.json,会将部分包的 dev 字段设置为 true;
  3. registry 的影响:安装源 registry 不同,执行 npm i 时也会修改 package-lock.json

使用 npm ci来 而不是 npm i 安装依赖,可以避免异常的修改 package-lock.json。

目前很多项目代码 lockfileVersion = 1,如果不小心更新node > 14,可能会导致 lockfileVersion = 2,而且会出现以下告警(v1=> npm v5 和 v6,v2: => npm v7&v8,向后兼容 v1 锁文件,v3: => npm v7&v8 没有向后兼容性):

npm WARN read-shrinkwrap This version of npm is compatible with lockfileVersion@1, but package-lock.json was generated for lockfileVersion@2. I'll try to do my best with it!

解决方案:需要在确定升级到 npm 8 和 package-lock.json 2 之前,对 npm 版本进行降级,比如npm install -g npm@6.14.15。如果是在 mac 或 linux 上需要先运行:rm /usr/local/bin/npm && ln -s ~/.npm-packages/bin/npm /usr/local/bin/npm。

3.4.1 初始化项目

首先在github创建一个仓库,协议选择MIT,gitignore选择Node,添加README.md描述文件。使用git clone将项目克隆到本地。cd 进入目录,使用vscode打开(终端输入code . 命令即可)。

然后创建一个合理的目录结构:

3.4.2 配置typescript

3.4.3 统一代码风格

首先,配置eslint,使用遵循Airbnb推出的JavaScript风格指南的eslint-config-airbnb,eslint-config-airbnb-typescript。

其次,配置prettier,安装依赖prettier,.prettierrc.js的推荐配置如下:

最后,增加git提交校验,安装依赖husky和lint-staged,其中,husky用于git 的hook,lint-staged用于针对暂存的 git 文件运行 linters。

在package.json中配置安装 husky 的命令,以及lint-staged检查:

执行npm run prepare 安装 husky,并在生成的.husky/pre-commit文件(如果没有生成,手动创建一个即可)中添加 npx lint-staged 命令:

All ready!执行git commit 提交代码就会触发 prettier和eslint自动修复。

3.4.3 配置babel

3.4.3 配置rollup

如果开发的是工具包,rollup更适合作为打包工具,如果是组件,比如vue组件,则@vue/cli 的 lib 模式更适合。根据开发环境区分不同的配置并在package.json的script中添加脚本命令,输出不同规范的产物:umd、umd.min、cjs、esm。

  1. 通用配置rollup.config.base.js

  2. 开发环境配置rollup.config.dev.js

  3. 正式环境配置rollup.config.prod.js

  4. 添加开发与打包相关的脚本命令到package.json,借助npm-run-all 依赖,npm run build按顺序执行z,buildjs,buildts。

  5. 添加支持tree shaking 的配置到package.json中。

3.4.4 发布到npm

添加发布到npm时需要忽略的文件与目录的配置,即.npmignore。

添加发布相关的脚本命令到package.json中,其中 npm --no-git-tag-version version 分别修改版本号中的major,minor,patch。

然后登录npm官网(npm login --registry https://registry.npmjs.org/),登录成功后,直接发布即可(npm publish --registry https://registry.npmjs.org/)。

发布过程中常见的报错:

  1. 400:版本问题,修改package.json的version即可;
  2. 401:npm源设置成第三方源(淘宝源)的时候发生的,比如我们经常会将镜像源设置成淘宝源。因此在发布时,应该使用默认的npm源登录,即npm login --registry https://registry.npmjs.org/;
  3. 403:包名重复,修改包名重新发布。

4 pnpm

pnpm 全称是 "Performant NPM",即高性能的 npm。它结合软硬链接与新的依赖组织方式,大大提升了包管理的效率,也同时解决了 "幻影依赖" 的问题,让包管理更加规范,减少潜在风险发生的可能性。

当项目中使用了一个没有在 package.json 文件中定义的包时,则出现幻影依赖(Phantom dependencies),也称为隐式依赖或幽灵依赖。项目中缺少声明的依赖最好被一个 bug, 它可能导致一些不符合预期错误,不易排查和处理:

  1. 不兼容的版本:项目的 package.json 声明包A,包A依赖包B,项目中使用到包B,如果对包A进行升级,而且升级的包A导致要使用升级的包B,项目中所有使用到包B的地方都可能出现问题,对开发者来说,只是升级包A,包B却出现了版本不兼容问题。
  2. 缺少依赖:包A是 devDependencies,即开发依赖,包A依赖包B,项目中使用到包。对于生成环境,不会安装包A的,也就不会安装包B,导致生产环境的依赖丢失。

5 babel

Babel 是一个 JavaScript 编译器。Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。Babel的能力:

  1. 语法转换,Babel也能转换JSX语法。
  2. 通过 Polyfill 方式在目标环境中添加缺失的功能(通过引入第三方 polyfill 模块,例如 core-js)。
  3. 源码转换(codemods),Babel 可以删除类型注释。
  4. Babel是完全插件化的。
  5. Babel 支持 Source map,因此可以轻松调试编译后的代码。
  6. Babel 尽最大可能遵循 ECMAScript 标准,也提供了特定的选项来对标准和性能做权衡。
  7. Babel 尽可能用最少的代码并且不依赖太大量的运行环境。也提供了"assumptions"选项来符合规范、文件大小和编译速度之间做权衡。

babel 的转译过程也分为三个阶段,这三步具体是:

  1. 解析 Parse: 将代码解析⽣成抽象语法树(AST),即词法分析与语法分析的过程;
  2. 转换 Transform: 对于 AST 进⾏⼀系列变换的操作,babel 接受得到 AST 并通过 babel-traverse 对其进⾏遍历,在此过程中进⾏添加、更新及移除等操作;
  3. ⽣成 Generate: 将变换后的 AST 再转换为 JS 代码, 使⽤到的模块是 @babel/generator。

6 vite

6.1为什么选择vite?

Webpack:

Vite:

  1. 开发服务器启动:当冷启动开发服务器时,基于打包器的方式启动必须优先抓取并构建整个应用,然后才能提供服务。而Vite首先将应用中的模块区分为依赖源码 两类。对于依赖 ,大多为在开发时不会变动的纯 JavaScript,Vite 将会使用 esbuild 预构建依赖,Go比以 Node.js 编写的打包器预构建依赖更快。对于源码,通常是会被编辑且需要转换的非JavaScript文件(比如jsx、tsx、css,、vue),Vite 以 原生 ESM 方式提供源码,让浏览器接管了打包程序的部分工作,Vite只需要在浏览器请求源码时进行转换并按需提供源码。
  2. 模块热替换:打包器支持了模块热替换(HMR),会将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活,但它也仍需要整个重新构建,因此即使采用了 HMR 模式,其热更新速度也会随着应用规模的增长而显著下降。而在Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界(大多数是模块本身)之间的链接失活。使得无论应用大小如何,HMR 始终能保持快速更新。
  3. 缓存:Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。
  4. 生产环境的构建:原生ESM由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2),为了在生产环境中获得最佳的加载性能,最好还是将代码进行 tree-shaking、懒加载和 chunk 分割(以获得更好的缓存),Vite使用的是Rollup,因为esbuild对于css和代码分割不是很友好。
  5. 生态不如webpack

7 esbuid

esbuild 使用 go 语言编写,由于相对 node 更为底层,且不提供 AST 操作能力,所以代码执行效率更高。可以减少等待构建运行的时间,能改善开发体验,但是esbuid相对较为底层,因此在拉入依赖关系和配置环境上环节上会花费时间。使用Rollup 和 terser 生成的bundler包相较略小 6.8 % 左右。

esbuild 有两大功能,分别是 bundler 与 minifier,其中 bundler 用于代码编译,类似 babel-loader、ts-loader;minifier 用于代码压缩,类似 terser。 但esbuild 无法操作 AST,所以一些需要操作 AST 的 babel 插件无法与之兼容,导致生产环境很少直接使用 esbuild 的 bundler 模块。幸运的是 minifier 模块可以直接替换为 terser 使用,可以用于生产环境。

8 eslint

ESLint 是在 ECMAScript/JavaScript 代码中识别和报告模式匹配的工具,它的目标是保证代码的一致性和避免错误。在许多方面,它和 JSLint、JSHint 相似,除了少数的例外:

  1. ESLint 使用 Espree 解析 JavaScript。
  2. ESLint 使用 AST 去分析代码中的模式
  3. ESLint 是完全插件化的。每一个规则都是一个插件并且你可以在运行时添加更多的规则。

在目录中存在package.json文件(可以使用npm init或yarn init创建)的前提下,运行安装和配置命令npm init @eslint/config后的目录中将有一个.eslintrc.{js,yml,json}文件。

相关推荐
m0_748254883 分钟前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl
ZJ_.15 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营19 分钟前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Cachel wood1 小时前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端1 小时前
0基础学前端-----CSS DAY9
前端·css
joan_851 小时前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
m0_748236111 小时前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
Watermelo6172 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_748248942 小时前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_748235612 小时前
从零开始学前端之HTML(三)
前端·html