文档知识来源:抖音 “哲玄前端”,《大前端全栈实践课》
注意:原抖音视频课程工程名为elpis,这里由于自己命名成了elips;
一、发布npm包前的准备工作
1、注册一个npm账号,并记住自己的账号名字;(我的账号:choukunbc081/elips)
2、抽离原本的elips,将业务的内容进行抽离,留下公共的框架能力发布成npm包,供大家使用;
3、在本地进行测试自己抽离的npm包
(1)elips使用 npm link 创建符号链接
(2)创建 elpis-demo 工程,并在 elpis-demo 使用 npm link @choukunbc081/elips 链接到 node_modules 目录下。
二、 抽离elips工程,elips-core
修改package.json
{
"name": "@choukunbc081/elips", // 包的唯一名字,此名称必须满足 NPM 的命名规范。使用npm账号
"version": "1.0.0", // 包的版本
"description": "", // 简短的包描述
"main": "index.js", // 指定包的入口文件,引入包时会执行的文件
"repository": {
"type": "git",
"url": "https://git.code.tencent.com/wangjmqz/elips.git"
},// 包的仓库地址
"author": "", // 包的作者
"license": "ISC", // 包的许可证 ...
}
由于elpis是暴露给外部用的,所以很多的插件不再是开发依赖,要挪到dependencies依赖中。
除了以下是devDependencies,其余依赖全部移动到dependencies。
创建index.js,入口文件
//引入 elpis-core
const ElipsCore = require('./elpis-core');
// 引入前端工程化构建方法
const FEBuildDev = require('./app/webpack/dev.js');
const FEBuildpRrod = require('./app/webpack/prod.js');
module.exports= {
/**
* 服务端基础
*/
Controller:{
Base: require('./app/controller/base.js')
},
Service:{
Base:require('./app/service/base.js')
},
/**
* 编译构建前端工程
* @param env 环境变量 local/production
*/
frontendBuild(env){
if(env === 'local'){
FEBuildDev();
}else if(env === 'production'){
FEBuildpRrod();
}
},
/**
* 启动 elips-core
* @param options 项目配置,透传到elips-core
*/
serverStart(options = {}){
const app = ElipsCore.start(options);
return app;
}
}
修改BFF层loader,将loader的各个都暴露出去
为了让其他使用npm包的人(自己本地测试,验证抽离内容,需创建工程elips-demo)可以进行自定义内容,elips-core/loader必须需要修改,把自身app目录下的配置和elips-demo app目录下的配置合并到一起;
elpis-core 是一个约定大于配置的服务框架,在使用时,只需要在对应的目录下写下相对应的文件,elpis-core 会帮你配置好其他的细节,并将内容挂载到 app 实例下。
重点:
-
将原有的loader配置,支持找到elips原有路径内容,也支持找到业务层路径内容;
-
例如: 原先的配置读取的是 elpis 路径下的文件,需要多读取一个使用 elpis 框架的项目(业务)路径
-
将原先挂载的操作封装为函数,然后将业务内容与 elpis 内容都挂载到相应的 loadr 上。
elpis 路径:
[ __dirname ]/../../app/ [ loader ]
业务路径:
process.cwd() / [ loader ]
以config.js为例
const path = require('path');
const { sep } = path;
/*
* config loader
* @param {object} app koa实例
*
* 配置区分 开发/测试/生产
* 目标:根据不同的环境(env.js),拿到不同文件配置(config.js) env.config
* 通过 env.config 覆盖 default.config 加载到 app.config 中
*
* 在目录下对应的 config 配置
* 默认配置 config/config.default.js
* 本地配置 config/config.local.js
* 测试配置 config/config.beta.js
* 生产配置 config/config.prod.js
* */
module.exports =(app)=> {
//1.找到 config/ 目录
const configPath = path.resolve(app.baseDir,`.${sep}config`);// /Users/apple/Desktop/前端系统架构/workspace/elips/config/config.default
// elips config 目录及相关文件
const elipsConfigPath = path.resolve(__dirname,`..${sep}..${sep}config`);
let defaultConfig = require(path.resolve(elipsConfigPath,`.${sep}config.default.js`));
//获取业务 config 目录及相关文件
const businessConfigPath = path.resolve(process.cwd(),`.${sep}config`);
//2. 获取 default.config
//为啥写try-catch,因为这个可以存在没有创建config.default文件
try {
// 将导入的配置展开并覆盖前面的配置
defaultConfig = {
...defaultConfig,
...require(path.resolve(businessConfigPath,`.${sep}config.default.js`))
}
}catch (e){
console.log('[exception] default.config file exception',e);
}
//3.获取 env.config
let envConfig = {};
try {
if(app.env.isLocal()){ //本地环境
envConfig = require(path.resolve(businessConfigPath,`.${sep}config.local.js`));
}else if(app.env.isBeta()){ //测试环境
envConfig = require(path.resolve(businessConfigPath,`.${sep}config.beta.js`));
}else if(app.env.isProduction()){//生产环境
envConfig = require(path.resolve(businessConfigPath,`.${sep}config.prod.js`));
}
}catch (e) {
console.log('[exception] env.config file exception',e);
}
//4. 覆盖并加载 config 配置
// app.config.xxxxx
app.config = Object.assign({},defaultConfig,envConfig);
};
在举例controller.js
const path = require('path');
const {sep } = path;
const glob = require('glob');
/*
* controller loader
* @param {object} app Koa实例
*
* 加载所有 controller ,可通过 ’app.controller.${目录}.${文件}' 访问
*
* 例子 :
* 文件关系
* app/controller
* |
* | -- custom-module
* |
* |-- custom-controller.js
*
* 可以这样拿到 => app.controller.customMoudel.customController
* 解答customMoudel 也就是目录custom-module,-需要用大写代替;
*
*/
module.exports = (app)=> {
const controller = {}; //目录
//1.读取 elips/app/controller/**/**.js 下所有文件
// 1>先获取路径
// path.resolve 会将路径转换为绝对路径; path.relative 来获取相对于 controllerPath 的相对路径
const elipsControllerPath = path.resolve(__dirname,`..${sep}..${sep}app${sep}controller`);
//2>读取所有的文件列表
const elipsFileList = glob.sync(path.resolve(elipsControllerPath,`.${sep}**${sep}**.js`));
elipsFileList.forEach(file =>{
handleFile(file)
});
//1.读取 业务/app/controller/**/**.js 下所有文件
const businessControllerPath = path.resolve(app.businessPath,`.${sep}controller`);
const businessFileList = glob.sync(path.resolve(businessControllerPath,`.${sep}**${sep}**.js`));
businessFileList.forEach(file =>{
handleFile(file)
});
//2.遍历所有文件目录,把内容加载到 app.controller 下
function handleFile(file){
//1> 提取文件名称
let name = path.resolve(file);
//2>截取路径, 举例: app/controller/custom-moudel/custom-controller.js 截取成 custom-moudel/custom-controller
name = name.substring(name.lastIndexOf(`controller${sep}`) + `controller${sep}`.length,name.lastIndexOf('.'));
//3> 把‘-’统一改为驼峰式,custom-module 目录 === customMoudel
// name = name.replace(/[-_](\w)/g, (_, c) => c.toUpperCase());
name = name.replace(/[_-][a-z]/ig,(s) => s.charAt(1).toUpperCase());
//4> 挂载 controller 到内存 app 对象中
let tempController = controller;
const names = name.split(sep);//[customMoudel,customController]
for (let i = 0,len = names.length;i<len; ++i) {
if(i === len -1){//文件
//path.resolve(file) 解析文件路径以确保它是绝对路径
//require(path.resolve(file)) 加载该文件内容
//require(path.resolve(file))(app) 调用这个中间件函数
//因为后面的controler都是一些class ---app/controller/view.js---这个文件return的是class
const ControlerMoule = require(path.resolve(file))(app);
tempController[names[i]] = new ControlerMoule();
}else{ //文件夹
//如果这个目录不存在,那么就初始化为一个对象
if(!tempController[names[i]]){
tempController[names[i]] = {};
}
tempController = tempController[names[i]];
//1.tempController === {customMoudel : {} }
//2.tempController === {customMoudel : { customController : { }}}
}
}
}
app.controller = controller;
};
三、抽离webpack
3.1 暴露webpack给使用方elips-demo
在 elpis 内部,我们分别提供了 dev.js 和 prod.js 来运行 webpack 的开发服务器和打包。我们需要将这些方法暴露出来,只需要将dev.js 和 prod.js 的功能封装成函数并暴露出去即可。
这部分写在了index.js中,也就是入口文件,请看上面第二点
3.2 抽离webpack内容
- webpack入口,支持业务方和自己
- 路径别名: 在将 eplis 当做包使用后之前的路径别名称会定位到我们的业务目录下,需要对此作出修改:
// 抽离前
resolve: {
extensions: ['.js', '.vue', '.less', '.css'], // 自动补全文件扩展名
alias: { $pages: path.resolve(process.cwd(), './app/pages'), // 路径别名
$common: path.resolve(process.cwd(), './app/pages/common'), // 路径别名
$widgets: path.resolve(process.cwd(), './app/pages/widgets'), // 路径别名
$store: path.resolve(process.cwd(), './app/pages/store'), // 路径别名 },
},
// 抽离后
resolve: {
$elpisPages: path.resolve(__dirname, "../../pages"), // 路径别名
$elpisBoot: path.resolve(__dirname, "../../pages/boot.js"),
$elpisCommon: path.resolve(__dirname, "../../pages/common"), // 路径别名
$elpisUtils: path.resolve(__dirname, "../../pages/common/utils.js"),
$elpisCurl: path.resolve(__dirname, "../../pages/common/curl.js"),
$elpisWidgets: path.resolve(__dirname, "../../pages/widgets"), // 路径别名
}
- 允许用户提供自己的 webpack 配置
webpack.base.js:
const glob = require('glob');
const path = require('path');
const fs = require('fs');
const webpack = require('webpack');
const merge = require('webpack-merge');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
//动态构造 elipsPageEntries elipsHtmlWebpackPluginList
const elipsPageEntries = {};
const elipsHtmlWebpackPluginList = [];
// 获取 app/pages 目录下所有入口文件(entry.xxx.js)
const elipsEntryList = path.resolve(__dirname,'../../pages/**/entry.*.js');
glob.sync(elipsEntryList).forEach(file =>{
handleFile(file,elipsPageEntries,elipsHtmlWebpackPluginList)
});
//动态构造 businessPageEntries businessHtmlWebpackPluginList
const businessPageEntries = {};
const businessHtmlWebpackPluginList = [];
// 获取 业务 app/pages 目录下所有入口文件(entry.xxx.js)
const businessEntryList = path.resolve(process.cwd(), './app/pages/**/entry.*.js');
glob.sync(businessEntryList).forEach(file =>{
handleFile(file,businessPageEntries,businessHtmlWebpackPluginList);
});
// 构造相关 webpack 处理的数据结构
function handleFile(file,entries,HtmlWebpackPluginList = []){
const entryName = path.basename(file,'.js');
// 构造 entry
entries[entryName] = file;
// 构造最终渲染的页面文件
HtmlWebpackPluginList.push(
// html-webpackplugin 辅助注入打包后的 bundle 文件到tpl文件中
new HtmlWebpackPlugin({
// 产物(最终模板) 输出路径
filename: path.resolve(process.cwd(),'./app/public/dist/',`${entryName}.tpl`),
// 指定要使用的模板文件
template: path.resolve(__dirname,'../../view/entry.tpl'),
// 要注入的代码快(这里需要和entry里的对应)
chunks: [entryName],
})
)
}
// 加载业务webpack 配置
let businessWebpackConfig ={};
try{
businessWebpackConfig = require(`${process.cwd()}/app/webpack.config.js`)
}catch(e){
console.log(e,"webpack-business-alias")
}
/**
* webpack 基础配置
*/
module.exports = merge.smart({
// 入口配置--->单页面,一个入口--entry: 'index',
// 多入口配置
entry: Object.assign({},elipsPageEntries,businessPageEntries),
// 模块解析配置(决定了要加载解释哪些模块,以及用什么方式去解释)--各种loader,例如css-loader/babel-loader
module: {
rules: [
{
test: /\.vue$/,
use: {
loader: require.resolve('vue-loader')
}
},{
test: /\.js$/,
include:[
// 对业务代码进行 babel, 加快 webpack 打包速度
//处理 elips 目录
path.resolve(__dirname,'../../pages'),
// 处理 业务 目录
path.resolve(process.cwd(),'./app/pages')
],
use: {
loader: require.resolve('babel-loader')
},
},{
test: /\.(png|jpe?g|gif)(\?.+)?$/,
use: {
loader: require.resolve('url-loader'),
options: {
limit: 300,
esModule: false
}
}
},{
test: /\.css$/,
use: [
require.resolve('style-loader'),
require.resolve('css-loader')],
},{
test: /\.less$/,
use: [
require.resolve('style-loader'),
require.resolve('css-loader'),
require.resolve('less-loader')],
},{
test: /\.(eoy|svg|ttf|woff|woff2)(\?\S*)?$/,
use: require.resolve('file-loader')
}
]
},
// 产出输出路径,因为开发和生产环境不一致,所以需要再各自的文件里面进行配置
output: {},
// 配置模块解析的具体行为(定义webpack 在打包时,如何找到并解析具体模块的路径)
//例如 imprort {xxxx} from './app/pages/xxx/xxx.js'或者//imprort {xxxx} from '../xxx.vue' ,找这种文件的后缀的一种配置
// 配置了alias之后,import {xxx} from '$elipsPages/xxx/xxx.js'
resolve: {
modules: [
'node_modules',
path.resolve(__dirname, '../../../node_modules'), // elips 的 node_modules
path.resolve(process.cwd(), 'node_modules'), // 业务项目的 node_modules
],
extensions: ['.js','.vue','.less','.css'],
extensions: ['.js','.vue','.less','.css'],
//别名
alias:(()=>{
const aliasMap ={};
const blankModulePath = path.resolve(__dirname,'../libs/blank.js');
//dashboard 路由扩展配置
const businessDashboardRouterConfig = path.resolve(process.cwd(),'./app/pages/dashboard/router.js');
aliasMap['$businessDashboardRouterConfig'] = fs.existsSync(businessDashboardRouterConfig) ? businessDashboardRouterConfig :blankModulePath;
// scheam-view component 扩展配置
const businessComponentConfig = path.resolve(process.cwd(),'./app/pages/dashboard/complex-view/schema-view/components/component-config.js');
aliasMap['$businessComponentConfig'] = fs.existsSync(businessComponentConfig) ? businessComponentConfig :blankModulePath;
// scheam-from component 扩展配置
const businessFormItemConfig = path.resolve(process.cwd(),'./app/pages/widgets/schema-form/form-item-config.js');
aliasMap['$businessFormItemConfig'] = fs.existsSync(businessFormItemConfig) ? businessFormItemConfig :blankModulePath;
// scheam-search component 扩展配置
const businessSearchItemConfig = path.resolve(process.cwd(),'./app/pages/widgets/schema-search-bar/search-item-config.js');
aliasMap['$businessSearchItemConfig'] = fs.existsSync(businessSearchItemConfig) ? businessSearchItemConfig :blankModulePath;
// header-container 扩展配置
const businessHeaderConfig = path.resolve(process.cwd(),'./app/pages/widgets/header-container/header-config.js');
aliasMap['$businessHeaderConfig'] = fs.existsSync(businessHeaderConfig) ? businessHeaderConfig :blankModulePath;
return {
'vue':require.resolve('vue'),
$elipsPages: path.resolve(__dirname,'../../pages'),
$elipsCommon: path.resolve(__dirname,'../../pages/common'),
$elipsCurl: path.resolve(__dirname,'../../pages/common/curl.js'),
$elipsUtils: path.resolve(__dirname,'../../pages/common/utils.js'),
$elipsWidgets: path.resolve(__dirname,'../../pages/widgets'),
$elipsHeaderContainer: path.resolve(__dirname,'../../pages/widgets/header-container/header-container.vue'),
$elipsSchemaForm: path.resolve(__dirname,'../../pages/widgets/schema-form/schema-form.vue'),
$elipsSchemaSearchBar: path.resolve(__dirname,'../../pages/widgets/schema-search-bar/schema-search-bar.vue'),
$elipsSchemaTable: path.resolve(__dirname,'../../pages/widgets/schema-table/schema-table.vue'),
$elipsSiderContainer: path.resolve(__dirname,'../../pages/widgets/sider-container/sider-container.vue'),
$elipsStore: path.resolve(__dirname,'../../pages/store'),
$elipsBoot:path.resolve(__dirname,'../../pages/boot.js'),
...aliasMap
}
})()
},
// 配置 webpack 插件
plugins: [
// 处理 .vue 文件,这个插件是必须的
// 它的只能是将你定义过的其他规则复制并应用到 .vue 文件里
// 例如,如果有一条匹配规则 /\.js/ 的规则,那么他会应用到 .vue 文件中的 <script> 模板中
new VueLoaderPlugin(),
// 第三方组件 把第三方库暴露到 window context 下
new webpack.ProvidePlugin({
Vue: 'vue',
axios: 'axios',
_: 'lodash'
}),
// 定义全局常量
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: true,// 支持 vue 解析 optionsApi
__VUE_PROD_DEVTOOLS__: false,// 在生产环境中禁用 Devtools 支持
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,// 关闭 vue.js 生产环境的客户端渲染警告 禁用生产环境显示 “水合”信息
}),
// 构造最终渲染的页面模板
/**
* 工程化打包的过程中,
* 1. path.resolve(process.cwd(),'./app/view/entry.tpl') 找到这个tpl
* 2. 找到 chunks: [entry.page2]的代码块,将 entry.page2 的代码块注入到 root 的 vue 的模板里面
* 3. 将注入好的产物,吐出到 filename: path.resolve(process.cwd(),'./app/public/dist/','entry.page2.tpl'), 这个路径下 ,文件名为 entry.page2.tpl
*/
...elipsHtmlWebpackPluginList,
...businessHtmlWebpackPluginList
],
// 配置打包输出优化(代码分割,模块合并,缓存,TreeShaing,压缩等优化策略)
// 为何要分包,是因为 如果多个文件(A、B)引用了同一个文件(C)内容是,这时打包只会是在A的里面有C,和B的里面有C,这样导致C重复
// 所以有了分包的概念,也就是把相同的内容就行提炼
optimization: {
// 在做分包,对产出的文件进行分包-splitChunks 这个的配置,是有你自己可以配置的
/**
* 哲哥分包策略
* 把 js 文件打包成3种类型
* 1. vendor : 第三方 lib ,基本不会动,除非依赖版本升级
* 2. common : 业务组件代码的公共部分抽取出来,改动较少
* 3. entry.{page}: 不同页面 entry 里的业务组件代码的差异部分,会经常改动
* 目的:把改动和引用评率不一样的 js 区分出来,已达到更好利用浏览器缓存的效果
*/
splitChunks: {
chunks: 'all',// 对同步和异步模块都进行分割
maxAsyncRequests: 10, // 每次异步加载的最大进行请求书
maxInitialRequests: 10,// 入口点的最大并行请求数
cacheGroups: {
vendor: { // 第三方依赖库
test:/[\\/]node_modules[\\/]/, // 打包node_module 中的文件
name: 'vendor', // 模块名称
priority: 20, // 优先级,数字越大,优先级越高
enforce: true, // 强制执行
reuseExistingChunk: true,//复用已有的公共模块chunk
},
common: { //公共模块
test: /[\\/]common|widgets[\\/]/,
name: 'common', // 模块名称
minChunks: 2 ,// 被两处引用 即被归为公共模块
minSize: 1,// 最小分割文件大小(1 byte)
priority: 10,// 优先级
reuseExistingChunk: true,// 复用已有的公共 chunk
}
}
},
// 将webpack 运行时生成的代码打包到 runtime.js
runtimeChunk: true,
},
},businessWebpackConfig)
四、提供公共组件能力
以 schema-view 为例,在 schema-view 中,我们提供了动态组件的扩展能力,需要为用户提供在外部扩展的能力。
import createForm from "./create-form/create-form.vue";
import editForm from "./edit-Form/edit-Form.vue";
import detailPanel from "./detail-panel/detail-panel.vue";
// 业务扩展 component 配置
import BusinessComponentConfig from '$businessComponentConfig';
const ComponentConfig = {
createForm:{
component: createForm
},
editForm:{
component:editForm
},
detailPanel:{
component:detailPanel
}
}
export default {
...ComponentConfig,
...BusinessComponentConfig
};
五、elips-demo 结构
详细抽离代码:git.code.tencent.com/wangjmqz/el…
``
六、npm包发布命令
npm config get
npm config set registry
npm login ---这里可以按回车键,会直接到浏览器进行登录,登陆成功后,返回控制台就会显示登录成功了
npm whoami
npm publish --会失败,因为这是一个私有的包
npm publish --access public --第一次提交需要告诉这是一个公有包
如果这一步失败,证明你是第一次注册npm
那么你需要先登录Npm之后,在进行创建token,然后再次执行npm publish --access public ,就会成功了;