将构建配置抽离成 npm 包的意义
- 通用性
- 业务开发这无需关注构建配置
- 统一团队构建脚本
- 可维护性
- 构建配置合理的拆分(开发、生产、ssr环境)
- README文档 changelog 文档等
- 质量
- 冒烟测试、单元测试、测试覆盖等
- 持续集成
可选方案
- 通过多个配置文件管理不同环境的配置
- 通过webpack --config参数来控制
- 将构建配置设计成一个库
- 抽成一个工具进行管理
- 将所有配置都放在一个文件,通过 --env 参数控制分支选择
构建配置包设计
通过多个配置文件管理不同环境的 webpack 配置
- 基础环境 webpack.base.js
- 开发环境 webpack.dev.js
- 生产环境 webpack.prod.js
- ssr 环境 webpack.ssr.js
- …
抽离成一个 npm 包统一管理
- 规范
- git commit 日志
- README
- ESlint规范
- Semver 规范
- 质量
- 冒烟测试
- 单元测试
- 测试覆盖率
- CI
操作方法:通过 webpack-merge 组合配置
merge = require('webpack-merge')
...
merge(
{ a[1], b: 5, c: 20 },
{ a[2], b: 10, d: 421 }
)
{ a: [1, 2], b: 10, c: 20, d: 421 }
合并配置
module.exports = merge( baseConfig, devConfig )
功能模块设计与目录结构
目录结构
- lib 放置源代码
- test 放置测试代码
新建项目
- 新建
builder-webpack
目录 cd builder-webpack
进入目录yarn init
初始化项目- 新建
lib
目录,其下新建四个文件- webpack.base.js
- webpack.dev.js
- webpack.prod.js
- webpack.ssr.js
配置 webpack.base.js 文件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const setMPA = () => {
let entry = {};
let htmlWebpackPlugins = [];
let HtmlWebpackExternalsPlugins = [];
let url = path.join(__dirname, '/src/*/index.jsx').replaceAll('\\', '/');
const entryFiles = glob.sync(url);
Object.keys(entryFiles).map(index => {
let entryFile = entryFiles[index];
let match = entryFile.match(/src\/(.*)\/index.jsx/);
const pageName = match && match[1];
if (pageName) {
entry[pageName] = entryFile;
// 每个入口文件设置 基础库的cdn
HtmlWebpackExternalsPlugins.push(new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://unpkg.com/react@18/umd/react.development.js',
global: 'React'
},
{
module: 'react-dom',
entry: 'https://unpkg.com/react-dom@18/umd/react-dom.development.js',
global: 'ReactDOM'
}
],
files: [`${pageName}.html`]
}));
// 入口文件生成模板
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `/src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: [pageName],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
})
);
}
});
return { entry, htmlWebpackPlugins, HtmlWebpackExternalsPlugins };
};
const { entry, htmlWebpackPlugins, HtmlWebpackExternalsPlugins } = setMPA();
module.exports = {
entry: entry,
output:{
filename: '[name].js',
path: path.join(__dirname, 'dist')
}
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ['babel-loader']
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['autoprefixer']
}
}
},
'less-loader'
]
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['autoprefixer']
}
}
}
]
},
{
test: /\.(png|svg|jpeg|jpg|gif|ico)$/i,
type: 'asset',
generator: {
filename: "static/img/[name].[hash:7][ext]"
}
}
]
},
plugins: [
new CleanWebpackPlugin(),
/* 命令行信息显示优化 */
new FriendlyErrorsWebpackPlugin(),
/* 打包捕获 error */
function () {
this.hooks.done.tap('done', stats => {
if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') == -1) {
console.log('build error');
process.exit(1);
}
});
},
/* CSS 提取成一个单独的文件 */
new MiniCssExtractPlugin({
filename: '[name]_[hash:8].css'
}),
]
.concat(htmlWebpackPlugins)
.concat(HtmlWebpackExternalsPlugins),
stats: 'errors-only'
};
安装 webpack-merge,配置 webpack.dev.js
yarn add webpack-merge -D
在 webpack.dev.js 中引入
const {merge} = requrie('webpack-merge')
const baseConfig = require('./webpack.base')
const devConfig = {}
module.exports = merge(baseConfig, devConfig)
const {merge} = requrie('webpack-merge');
const webpack = require('webpack');
const baseConfig = require('./webpack.base');
const devConfig = {
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
devServer: {
port: 3000,
compress: false,
static: {
directory: path.join(process.cwd(), 'dist'),
publicPath: '/'
},
client: {
overlay: {
errors: true,
warnings: false
}
},
stats: 'errors-only'
},
devtools: 'cheap-source-map'
};
module.exports = merge(baseConfig, devConfig);
配置 webpack.prod.js
同样是使用 webpack-merge
const merge = require('webpack-merge');
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
const baseConfig = require('./webpack.base');
const prodConfig = {
mode: 'production',
/* 设置提取的公共文件包的大小 */
optimization: {
/* 压缩 css */
minimize: true,
minimizer: [
new CssMinimizerWebpackPlugin()
],
splitChunks: {
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2
}
}
}
},
plugins: [
/* 速度优化: 引入基础包的 cdn */
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://unpkg.com/react@18/umd/react.development.js',
global: 'React'
},
{
module: 'react-dom',
entry: 'https://unpkg.com/react-dom@18/umd/react-dom.development.js',
global: 'ReactDOM'
}
]
})
]
};
module.exports = merge(baseConfig, prodConfig);
optimize-css-assets-webpack-plugin
插件配合cssnano
css 处理器来压缩处理配置中 ExtractTextPlugin 实例导出的文件的文件名运行,而不是 源css文件的文件名,默认为/\.css$/g
- assetNameRegExp 一个正则表达式,指示应优化的最小化的资源的名称
- 提供的正则表达式针对配置中
ExtractTextPlugin
实例导出的文件的文件名运行,而不是源css 文件的文件名,默认是/\.css$/g
- 提供的正则表达式针对配置中
- cssProcessor 用于优化最小化 css 的
css 处理器
,默认是cssnano
- 这应该是一个跟随
cssnano.processor
接口的函数(接收 css 和 选项参数并返回一个 Promise)
- 这应该是一个跟随
- cssProcessorOptions 传递给 cssProcessor 的选项,默认为 {}
- cssProcessorPluginOptions 传递给 CSSProcessor 的插件选项,默认为 {}
- canPrint 一个布尔值,指示插件是否可以将信息打印到控制台,默认为 true
配置 webpack.ssr.js
拷贝一份 webpack.prod.js
,需要设置忽略解析 css
{
module:{
rules:[
{
test:/\.css$/,
use:[
'ignore-loader'
]
},
{
test:/\.less$/,
use:[
'ignore-loader'
]
}
]
}
}
const merge = require('webpack-merge');
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
const baseConfig = require('./webpack.base');
const prodConfig = {
module: {
rules: [
{
test: /\.css$/,
use: ['ignore-loader']
},
{
test: /\.less$/,
use: ['ignore-loader']
}
]
},
mode: 'production',
/* 设置提取的公共文件包的大小 */
optimization: {
/* 压缩 css */
minimize: true,
minimizer: [
new CssMinimizerWebpackPlugin()
],
splitChunks: {
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2
}
}
}
},
plugins: [
/* 速度优化: 引入基础包的 cdn */
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://unpkg.com/react@18/umd/react.development.js',
global: 'React'
},
{
module: 'react-dom',
entry: 'https://unpkg.com/react-dom@18/umd/react-dom.development.js',
global: 'ReactDOM'
}
]
})
]
};
module.exports = merge(baseConfig, prodConfig);
其他文件
- 根目录新建 README.md 文件
- .gitignore 设置 git 忽略文件
/node_modules
/logs
通过 ESLint 规范构建脚本
使用 eslint-config-airbnb-base
eslint --fix
可自动处理空格
module.exports = {
"parser": "babal-eslint",
"extends": "airbnb-base",
"env": {
"browser": true,
"node": true
}
}
- 安装
eslint
、@babel/eslint-parser
、@babel/core
、eslint-plugin-import
、eslint-config-airbnb-base
yarn add eslint @babel/eslint-parser @babel/core eslint-plugin-import eslint-config-airbnb-base -D
- 配置
eslint
配置文件.eslintrc.js
module.exports = {
"parser": "@babel/eslint-parser",
"parserOptions": {
"requireConfigFile": false
},
"extends": "airbnb-base",
"env": {
"browser": true,
"node": true
}
};
package.json
设置scripts
脚本
{
"scripts": {
"fix": "eslint ./lib --fix",
"eslint": "eslint ./lib"
},
}
- 使用(根据 eslint 提示对代码进行调整)
yarn run eslint
yarn run fix
可自动更正一些格式问题
冒烟测试(预测试)
指 对提交测试的软件在进行详细深入的测试之前进行的预测试
主要目的是 暴露导致软件需要重新发布的基本功能失效等严重问题
关注问题
-
构建是否成功
-
每次构件完成 build 目录是否有内容输出
- 是否有 JS、CSS 等静态资源文件
- 是否有 HTML 文件
每次都手动执行,比较繁琐,可通过一些工具(mocha)来完成这一步骤
检测构建(清空dist、判断是否构建成功)
在示例项目中原型构建,看是否有报错
-
根目录下创建 test 目录
-
test/smoke/index.js 再次书写冒烟测试相关的代码
-
需要判断否写构建是否正常运作,需要有一个模板项目
-
新建 test/smoke/template 目录下创建模板项目
- 拷贝一个项目到 template 目录下,删除 该项目相关的webpack 配置(webpack.prod.js 等文件)
-
需要
rimraf
这个库来处理删除dist
目录这个操作- 每次构建之前否需要将
dist
目录清空 - 执行删除操作之后,会执行一个回调函数
yarn add rimraf -D
- 每次构建之前否需要将
-
test/smoke/index.js
- 首先删除 dist 目录,清除上次构建内容
- 引入配置文件 webpack.prod.js 文件
- webpack 方法接收一个配置文件,后执行回调函数,在回调函数中捕获错误信息,若构建有问题,打印错误信息
const path = require('path');
const webpack = require('webpack');
const rimraf = requir('rimraf');
/* 需要现将目录切换到 template */
process.chdir(path.join(__dirname, 'template'));
rimraf('./dist', () => {
const prodConfig = require('../../lib/webpack.prod');
webpack(prodConfig, (err, stats) => {
if (err) {
console.log(err);
process.exit(2);
}
console.log(stats.toString({
colors: true,
modules: false,
children: false
}));
});
});
- 执行 node index.js 文件
node ./test/smoke/index.js
- 模板项目中路径均使用 __dirname(以template为根) 来配置路径,需要使用
process.cwd()
来获取整个项目的根路径- webpack.base.js 中定义一个全局变量 projectRoot 来替换 __dirname
webpack.prod.js
const { merge } = require('webpack-merge');
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
const baseConfig = require('./webpack.base');
const prodConfig = {
mode: 'production',
/* 设置提取的公共文件包的大小 */
optimization: {
/* 压缩 css */
minimize: true,
minimizer: [
new CssMinimizerWebpackPlugin()
],
splitChunks: {
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2,
},
},
},
},
plugins: [
/* 速度优化: 引入基础包的 cdn */
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://unpkg.com/react@18/umd/react.development.js',
global: 'React',
},
{
module: 'react-dom',
entry: 'https://unpkg.com/react-dom@18/umd/react-dom.development.js',
global: 'ReactDOM',
},
],
}),
],
};
module.exports = merge(baseConfig, prodConfig);
webpack.base.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
const glob = require('glob');
const projectRoot = process.cwd();
/* /builder-webpack/test/smoke/template/ */
const setMPA = () => {
const entry = {};
const htmlWebpackPlugins = [];
const url = path.join(projectRoot, '/src/*/index.jsx').replaceAll('\\', '/');
const entryFiles = glob.sync(url);
Object.keys(entryFiles).map((index) => {
const entryFile = entryFiles[index];
const match = entryFile.match(/src\/(.*)\/index.jsx/);
const pageName = match && match[1];
if (pageName) {
entry[pageName] = entryFile;
// 入口文件生成模板
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.join(projectRoot, `/src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: [pageName],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false,
},
}),
);
}
return htmlWebpackPlugins;
});
return { entry, htmlWebpackPlugins };
};
const { entry, htmlWebpackPlugins } = setMPA();
console.log(entry);
module.exports = {
entry: entry,
output: {
filename: '[name].js',
path: path.join(projectRoot, 'dist')
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ['babel-loader'],
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['autoprefixer'],
},
},
},
'less-loader',
],
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['autoprefixer'],
},
},
},
],
},
{
test: /\.(png|svg|jpeg|jpg|gif|ico)$/i,
type: 'asset',
generator: {
filename: 'static/img/[name].[hash:7][ext]',
},
},
],
},
plugins: [
new CleanWebpackPlugin(),
/* 命令行信息显示优化 */
new FriendlyErrorsWebpackPlugin(),
/* 打包捕获 error */
function doneErrorPlugin() {
this.hooks.done.tap('done', (stats) => {
if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') === -1) {
process.exit(1);
}
});
},
/* CSS 提取成一个单独的文件 */
new MiniCssExtractPlugin({
filename: '[name]_[hash:8].css',
}),
]
.concat(htmlWebpackPlugins),
resolve: {
alias: {
'@': path.join(projectRoot, '/'),
'@server': path.join(projectRoot, '/server'),
'@src': path.join(projectRoot, '/src'),
'@comp': path.join(projectRoot, '/src/components'),
'@images': path.join(projectRoot, '/src/assets/images'),
}
},
stats: 'errors-only',
};
创建测试文件
-
/test/smoke/
目录下新建两个测试文件- html-test.js 检测是否生成 HTML 文件
- css-js-test.js 检测是否生成 css、js 文件
-
安装 mocha
yarn add mocha -D
- 安装 glob-all
yarn add glob-all -D
html-test.js
const glob = require('glob-all');
describe('Checking generated html files', () => {
it('should generate html files', done => {
const files = glob.sync([
'./dist/index.html',
'./dist/search.html',
]);
if (files.length > 0) {
done();
} else {
throw new Error('no html files generate');
}
});
});
css-js-test.js
const glob = require('glob-all');
describe('Checking generated css js files', () => {
it('should generate css js files', done => {
const files = glob.sync([
'./dist/index_*.js',
'./dist/index_*.css',
'./dist/search_*.js',
'./dist/News_*.js',
]);
if (files.length > 0) {
done();
} else {
throw new Error('no css js files generate');
}
});
});
- 在 index.js 中
- 新建 mocha 实例
- 使用 实例方法
addFile()
引入以上两个测试文件 - 使用 实例方法
run()
运行 mocha
const path = require('path');
const webpack = require('webpack');
const rimraf = require('rimraf');
const Mocha = require('mocha');
/* 设置过期时间 */
const mocha = new Mocha({
timeout: '10000ms'
});
/* 需要现将目录切换到 template */
process.chdir(path.join(__dirname, 'template'));
rimraf('./dist', () => {
const prodConfig = require('../../lib/webpack.prod');
webpack(prodConfig, (err, stats) => {
if (err) {
console.log(err);
process.exit(2);
}
console.log(stats.toString({
colors: true,
modules: false,
children: false
}));
+ console.log('Webpack build success, begin run test');
+ mocha.addFile(path.join(__dirname, 'html-test.js'));
+ mocha.addFile(path.join(__dirname, 'css-js-test.js'));
+ mocha.run();
});
});
单元测试与测试覆盖率
冒烟测试保证了 构建包的基本功能可用
细节部分的把控需要单元测试来完成
可选方案
-
单纯测试框架,需要断言库(chai / should.js / expect / better-assert)
- mocha 框架
- ava 框架
-
集成框架,开箱即用
- Jasmine 框架
- Jest 框架
-
极简 API
编写单元测试用例
- 技术选型: Mocha + Chai
- 测试代码: describe, it, expect
- 测试命令: mocha add.test.js
-
test 目录下新建 unit 目录用来编写测试用例
- 创建对应的测试文件 webpack-base-test.js
describe('webpack.base.js test case', () => { const baseConfig = require('../../lib/webpack.base'); console.log(baseConfig); it('entry', () => { }); });
-
test 目录下新建 index.js 作为单元测试的入口文件(主要用来引入对应的单元测试用例)
- 每次执行测试用例之前需要进入到模板项目
template
中去 - 因此使用
process.chdir()
切换路径到 template 目录
const path = require('path'); process.chdir(path.join(__dirname, 'smoke/template')); describe('builder-webpack test case', () => { require('./unit/webpack-base-test'); });
- 每次执行测试用例之前需要进入到模板项目
-
package.json
新建scripts
{ "scripts": { "test": "./node_modules/.bin/_mocha" } }
执行
yarn run test
时就会自动访问test
目录下index.js
入口文件yarn run test
-
为判断
每次构建是否影响 entry
使用断言库assert
-
安装 assert
yarn add assert -D
const assert = require('assert'); describe('webpack.base.js test case', () => { const baseConfig = require('../../lib/webpack.base'); // console.log(baseConfig); it('entry', () => { assert.equal(baseConfig.entry.index, 'D:/Z-workSpace/React/ssr-react/builder-webpack/test/smoke/template/src/index/index.jsx'); assert.equal(baseConfig.entry.search, 'D:/Z-workSpace/React/ssr-react/builder-webpack/test/smoke/template/src/search/index.jsx'); }); });
此时测试用例已经跑通了
测试覆盖率
推荐使用 nyc
- 安装
yarn add nyc -D
- 修改 scripts 脚本
{
"scripts": {
"test": "mocha",
"coverage": "npx nyc mocha"
}
}
单元测试与冒烟测试
单元测试和测试覆盖率一般是在构建前还是构建后完成?
- 针对
基础组件
或者构建包
,通过单元测试和测试覆盖率
保证组件质量,这需要在发版之前严格遵守
- 对业务而言,每次
commit
就会进行业务代码的构建。同时异步触发单元测试和测试覆盖率检查,无先后顺序
持续集成
优点
- 快速发现错误
- 防止分支大幅偏离主干
核心措施
代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成
接入 github action
-
首先创建一个 github 项目,命名为
builder-webpack
-
项目根目录下创建
.github/workflow/test.yml
编写 yml 脚本设置自动化测试即可 -
git 链接远程仓库
# 克隆远程仓库到一个干净的目录 git clone <远程仓库地址> # 进入项目 cd builder-webpack[项目名称] # 查看当前目录绝对路径 pwd # 将源码复制到该目录下(-r 表示递归,对多个文件操作) cp -r [被复制目录 ./***/ ] [当前目录 ./] cd ../../ cp -r ../ssr-react/builder-webpack/ ./
-
编写
github action
自动化测试脚本test.yml
# This is a basic workflow to help you get started with Actions name: CI # Controls when the workflow will run on: # Triggers the workflow on push or pull request events but only for the "main" branch push: branches: [ "main" ] pull_request: branches: [ "main" ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 - name: setup node.js environment uses: actions/setup-node@v3.3.0 with: node-version: "18.X" - name: install dep run: yarn install -D - name: switch to template project run: cd ./test/smoke/template - name: install template project dep run: yarn install -D - name: run test scripts run: yarn run test
-
提交代码到远程仓库
git add . git commit -m 'feat: builder github action ci' git push origin [branch_name]
-
由于之前是在本地测试,entry 入口是本地目录,推送到 github 之后需要修改
webpack-base-test.js
单元测试脚本assert.equal(baseConfig.entry.index, '/home/runner/work/builder-webpack/builder-webpack/test/smoke/template/src/index/index.jsx'); assert.equal(baseConfig.entry.search, '/home/runner/work/builder-webpack/builder-webpack/test/smoke/template/src/search/index.jsx');
具体构建信息可在 github 项目 action 中查看
-
test.yml
设置依赖缓存,减少每次构建安装依赖的时间name: CI on: push: branches: [ "main" ] pull_request: branches: [ "main" ] workflow_dispatch: jobs: test: runs-on: ubuntu-latest steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 - name: setup node.js environment uses: actions/setup-node@v3.3.0 with: node-version: "18.X" - name: Cache node_modules id: cache-node-modules uses: actions/cache@v1 with: path: node_modules key: ${{ runner.os }}-${{ matrix.node-version }}-nodeModules-${{ hashFiles('package.json') }} restore-keys: | ${{ runner.os }}-${{ matrix.node-version }}-nodeModules- - name: install dep if: steps.cache-node-modules.outputs.cache-hit != 'true' run: yarn install -D - name: switch to template project run: cd ./test/smoke/template - name: Cache node_modules id: cache-node-modules-template uses: actions/cache@v2 with: path: node_modules key: ${{ runner.os }}-${{ matrix.node-version }}-nodeModules-${{ hashFiles('package.json') }} restore-keys: | ${{ runner.os }}-${{ matrix.node-version }}-nodeModules- - name: install template project dep if: steps.cache-node-modules-template.outputs.cache-hit != 'true' run: yarn install -D - name: run test scripts run: yarn run test
发布构建包到 npm 社区
- 修改
package.json
中 name 字节(确保npm 社区
中该包名没有被使用过) - 升级版本
- 补丁版本号
npm version patch
- 小版本号
npm version minor
- 大版本号
npm version major
- 补丁版本号
# 登录 npm
npm login
# 升级版本
npm version minor
# 发布构建包
npm publish