普通视图

发现新文章,点击刷新页面。
今天 — 2026年5月7日首页

大前端全栈实践:章节五(npm包抽离)

作者 RichardZhiLi
2026年5月6日 20:08

截止到这个章节,我们的elpis项目基本上已经算是开发完成了。

回顾前几个章节,我们完成了需求确立 -> 系统架构设计 -> 实现 -> 应用检验四个流程。现在是时候将其抽象为npm包发布并开放给我们真实业务项目使用了。

本篇文章重点聚焦在抽离业务逻辑,封装成npm包重构代码的过程

一、源码重构篇

  1. 开放框架入口(能力)

在重构的过程中,第一个遇到的难题就是需要判断和决定咱们所做的这份框架,它应该对外暴露哪些能力,在思考这一点的时候,不妨回归框架本身设计的目的,以elpis为例。这份框架它本质上是一个读取领域模型(配置文件)后输出从前端到后端一体化打包产物的全栈框架**。在此之中,读取领域模型是通过我们的后端服务来完成的,我们通过约定用户将所有的配置文件放置在app/model目录下,在后端服务启动时会自动读取并加载。那么剩下的所需要暴露的,只有前端/后端的构建能力了。因此改造实施步骤如下

原有项目入口是下面这样的,当时我们在开发项目时为了图方便,直接引入了我们自定义的服务然后便直接启动了

elpis/index.js
const ElpisCore = require('./elpis-core');
ElpisCore.start(
    {
        name: "Richard's Elpis",
        homePage: '/view/project-list',
    }
);

这样做明显是不行的,为什么?因为如果这样做用户导入框架时就会立马自行启动我们服务了,这肯定不是我们所期望的。我们所期望的是用户能够自行决定什么时候启动我们的服务(器),因此我们需要将其抽象为一个函数,比如说就叫做serverStart,并暴露给用户来自行决定调用,那么我们应该改写为下列形式。

elpis/index.js
module.exports = {
    serverStart(options={}){
        ElpisCore.start(options)
    }
}

同时我们也能够注意到,仅仅只是启动服务可能并不能够完全方便用户使用。例如有时候用户想要在开发过程中自行调试或者增加些中间件什么的,那么在此时我们就需要将服务器内部核心挂载对象暴露给我们的用户。在这个例子中,由于我们底层使用的是Koa2来完成的,因此最简单的实现方式就是使用一个变量接住这个对象并返回回去,于是便有了

elpis/index.js
serverStart(options={}){
        const app = ElpisCore.start(options)
        return app;
 }

到了这一步你就应该能注意到,其实这一个操作过程对应着我们上述思考过程中的“前端到后端一体化打包产物”的后端构建部分。因为后端在用户视角看来本质上就是一个运行中的服务器,它所提供的服务,并不存在什么打包产物可言,因此到这一步为止我们已经完成了暴露后端的部分

紧接着就是考虑如何暴露前端。我们也是模仿暴露后端的思路,做一个暴露前端的函数,比如就叫做frontendBuild。当用户调用它的时候,本质上就是在运行我们内部的webpack构建命令,并执行构建操作。 这些构建命令和指令在前面工程化的章节已讲解过,并且也讲解了环境区分,这里不再赘述。

elpis/index.js
const FEBuildDev = require('./app/webpack/dev')
const FEBuildProd = require('./app/webpack/prod')
module.exports = {
    frontendBuild(env){
    if(env === 'local'){
        FEBuildDev()
    } else {
        FEBuildProd()
    }
    }
}

最终完成后的样子

elpis/index.js
const ElpisCore = require('./elpis-core');
const FEBuildDev = require('./app/webpack/dev')
const FEBuildProd = require('./app/webpack/prod')
module.exports = {
    serverStart(options={}){
        const app = ElpisCore.start(options)
        return app;
     }
    ,
    frontendBuild(env){
        if(env === 'local'){
            FEBuildDev()
        } else {
            FEBuildProd()
        }
    }
}

一旦我们定义好了我们对外暴露的能力(函数)后,后续的改造思路就很清晰了,只需要沿着我们所导入的模块(比如elpis-core/index.js 以及app/webpack/dev.js,app/webpack/prod.js) 根据报错信息顺藤摸瓜的去改造内部对应的函数/代码块就好了。

小提示:

想要本地调试自己本地开发的框架的话,可以使用npm link命令。 首先进入自己的框架项目下,运行npm link。再去到一个Demo项目目录下,执行npm link 你的包名,这样就成功将你的框架项目引入过来了。会在对应的Demo目录的node_modules下方体现出来。以我的elpis项目为例, 那么我在elpis项目目录下,执行npm link 再去到elpisDemo目录下,执行npm link @richardzhili/elpis 那么就会在我的elpisDemo/node_modules下生成@richardzhili/elpis这个包

引入.png

启动项目.png 需要注意的是,应用项目在使用npm link 包名的时候,必须和框架项目package.json下的name对应一致,否则会引入失败

  1. loader改造 顺着上面的思路,我们在elpis-demo中引入elpis后,启动会发现报错,报错信息来自于各个loader。我们以其中一个controller loader为例子进行分析
elpis-core/loader/controller.js
const path = require('path');
const glob = require('glob');
const { sep } = path;
/**
 * controller loader
 * 加载所有controller,可通过'app.controller.${目录}.${文件}'访问
 * 例子:
 * app/controller
 *  |
 *  | -- custom-module
 *            |
 *            | -- custom-controller.js
 *  => app.controller.customModule.customController //通过这种方式访问对应的controller
 * 其内部实现和middleware类似,区别在于controller目录下的文件都是类,但是middleware目录下的模块都是可执行函数,所以需要new一下成为实例后挂载到app对象上
 * @param {object} app Koa 实例
 */
module.exports = (app) => {
    //读取app/controller/**/*.js下的文件
    const controllerPath = path.resolve(app.businessPath, 'controller');
    const fileList = glob.sync(path.resolve(controllerPath, `.${sep}/**${sep}*.js`));
    //遍历所有文件目录,把内容加载到app.middlewares对象上
    const controller = {};
    fileList.forEach((filePath) => {
        const filePathArr = filePath.split(`${sep}`);
        const controllerPathArr = filePathArr.filter((item, index) => index > filePathArr.indexOf('controller')); //过滤出controller/xxx/xxx后续的文件名称和目录名称
        controllerPathArr.forEach((name, index, thisArg) => {
            name = name.replace(/[_-][a-z]/ig, (s) => s.charAt(1).toUpperCase());//返回匹配到的-_号的后面一个字母,并将这个字母转换为大写并返回。寓意在于将短横线命名改为驼峰命名
            name = name.replace('.js', ''); //去掉.js后缀
            thisArg[index] = name;
        })
        //根据切分好的驼峰文件名称,将controller对象进行层层嵌套,最终把文件内容加载到对应的层级上
        //例如app/controller/custom-module/custom-module-entry.js -> app.controller.customModule.customModuleEntry
        let tempController = controller;
        for (let i = 0; i < controllerPathArr.length; i++) {
            if (i === controllerPathArr.length - 1) {
                const ControllerClass = require(filePath)(app);
                tempController[controllerPathArr[i]] = new ControllerClass(); //把controller文件导出的类new一下成为实例挂载到app.controller上
                return;
            }
            if (!tempController[controllerPathArr[i]]) {
                tempController[controllerPathArr[i]] = {};
            }
            tempController = tempController[controllerPathArr[i]];
        }
    })
    app.controller = controller; //把加载好的controller对象挂载到app.controller上
}

报错提示是因为找不到对应的目录和文件(app.businessPath和controllerPath),即app/controllers目录。这个也很正常,因为我们的项目启动之初,定义的app.businessPath为process.cwd() + '/app' process.cwd() + '/app' + 'controller',而process.cwd()在运行时变成了用户项目的根目录,而用户项目没有建立对应的app/controller目录和对应文件,所以才会报这个错误。解决方案也很简单,就是在elpisDemo项目下建立对应目录就好了,这个是我们框架的约定。而这个错误并不是我们想讨论的话题,我们真正想讨论的话题是,我们这种写法虽然能够引入用户定义的controller,但是好像我们自己沉淀的那些个controller,即预置在框架内部的controller,好像就没法读进来了? 所以,我们需要改造原有的代码,从而保证我们框架内部沉淀的controller能够一并加载进来,并与用户的合并。

因此,我们需要增加一段读取我们框架项目contoller目录的逻辑,并执行同样的文件处理函数。于是便将处理文件的函数封装为了'handleFile',同时我们自己沉淀的controller,通过__dirname这个魔术变量引入

elpis-core/loader/controller.js
const path = require('path');
const glob = require('glob');
const { sep } = path;
/**
 * controller loader
 * 加载所有controller,可通过'app.controller.${目录}.${文件}'访问
 * 例子:
 * app/controller
 *  |
 *  | -- custom-module
 *            |
 *            | -- custom-controller.js
 *  => app.controller.customModule.customController //通过这种方式访问对应的controller
 * 其内部实现和middleware类似,区别在于controller目录下的文件都是类,但是middleware目录下的模块都是可执行函数,所以需要new一下成为实例后挂载到app对象上
 * @param {object} app Koa 实例
 */
module.exports = (app) => {

    //遍历所有文件目录,把内容加载到app.controller对象上
    const controller = {};

    //读取elpis-core/app/controller/**/*.js下的文件
    const elpisControllerPath = path.resolve(__dirname, '..', '..', 'app', 'controller');
    const elpisFileList = glob.sync(path.resolve(elpisControllerPath, `.${sep}/**${sep}*.js`));
    elpisFileList.forEach(handleFile)

    //读取app/controller/**/*.js下的文件
    const businessControllerPath = path.resolve(app.businessPath, 'controller');
    const businessFileList = glob.sync(path.resolve(businessControllerPath, `.${sep}/**${sep}*.js`));
    businessFileList.forEach(handleFile)

    function handleFile(filePath) {
        const filePathArr = filePath.split(`${sep}`);
        const controllerPathArr = filePathArr.filter((item, index) => index > filePathArr.indexOf('controller')); //过滤出controller/xxx/xxx后续的文件名称和目录名称
        controllerPathArr.forEach((name, index, thisArg) => {
            name = name.replace(/[_-][a-z]/ig, (s) => s.charAt(1).toUpperCase());//返回匹配到的-_号的后面一个字母,并将这个字母转换为大写并返回。寓意在于将短横线命名改为驼峰命名
            name = name.replace('.js', ''); //去掉.js后缀
            thisArg[index] = name;
        })
        //根据切分好的驼峰文件名称,将controller对象进行层层嵌套,最终把文件内容加载到对应的层级上
        //例如app/controller/custom-module/custom-module-entry.js -> app.controller.customModule.customModuleEntry
        let tempController = controller;
        for (let i = 0; i < controllerPathArr.length; i++) {
            if (i === controllerPathArr.length - 1) {
                const ControllerClass = require(filePath)(app);
                tempController[controllerPathArr[i]] = new ControllerClass(); //把controller文件导出的类new一下成为实例挂载到app.controller上
                return;
            }
            if (!tempController[controllerPathArr[i]]) {
                tempController[controllerPathArr[i]] = {};
            }
            tempController = tempController[controllerPathArr[i]];
        }
    }
    app.controller = controller; //把加载好的controller对象挂载到app.controller上
}

其他的所有loader都是相同的处理逻辑,增加了__dirname导入我们自己项目底下的对应部分,然后将文件处理函数封装并为我们的文件调用。

另外,中间件我们在启动服务器时,也仅仅只是从我们自己的项目中引入,这个地方也需要改造为从用户的目录中引入,并与我们自己的中间件合并。 原来的服务器启动文件

elpis-core/index.js
const Koa = require('koa');
const path = require('path');
const env = require('./env');
const configLoader = require('./loader/config'); //配置解析器
const extendLoader = require('./loader/extend'); //扩展解析器
const serviceLoader = require('./loader/service'); //服务解析器
const controllerLoader = require('./loader/controller'); //控制器解析器
const middlewareLoader = require('./loader/middleware'); //中间件解析器
const routerSchemaLoader = require('./loader/router-schema'); //路由参数校验解析器
const routerLoader = require('./loader/router'); //路由解析器
module.exports = {
    /**
     * ElpisCore启动函数(主入口)
     * @param {*} options 项目配置
     * options = {
     * name //项目名称
     * homePath //项目主页路径
     * }
     */
    start(options = {}) {
        const app = new Koa(); //初始化Koa实例

        /**
         * 配置调整
         */
        app.options = options;
        app.baseDir = process.cwd(); //基础路径
        app.businessPath = path.join(app.baseDir, 'app'); //业务代码路径
        app.env = env(); //环境变量
        console.log('elpis-core running in environment: ' + app.env.get());

        //调用各个loader
        configLoader(app);
        console.log('load config loader success');
        extendLoader(app);
        console.log('load extend loader success');
        serviceLoader(app);
        console.log('load service loader success');
        controllerLoader(app);
        console.log('load controller loader success');
        middlewareLoader(app);
        console.log('load middleware success');
        routerSchemaLoader(app);
        console.log('load router schema loader success');
        try {
            require(path.resolve(app.businessPath, 'middleware.js'))(app)
            console.log('load global middleware done');
        }
        catch (err) {
            console.error('[exception] there is no global middleware file.')
        }
        routerLoader(app);
        console.log('load router loader success');
        /**
         * try catch 捕获,并启动监听
         */
        try {
            const port = process.env.PORT || 8080;
            const host = process.env.HOST || '0.0.0.0';
            app.listen(port, host);
            console.log(`Server running at http://${host}:${port}/`);
        }
        catch (err) {
            console.error(err);
        }
        return app;
    }
}

增加部分

elpis-core/index.js
 //注册elpis全局中间件
        const elpisMiddlewarePath = path.resolve(__dirname, '..', 'app', 'middleware.js');
        try {
            require(elpisMiddlewarePath)(app)
            console.log('[start] load global elpis middleware done');
        }
        catch (err) {
            console.error('[exception] there is no global elpis middleware file.')
        }


        //注册业务全局中间件
        try {
            require(path.resolve(app.businessPath, 'middleware.js'))(app)
            console.log('[start] load global business middleware done');
        }
        catch (err) {
            console.error('[exception] there is no global business middleware file.')
        }

最终版本:

const Koa = require('koa');
const path = require('path');
const env = require('./env');
const configLoader = require('./loader/config'); //配置解析器
const extendLoader = require('./loader/extend'); //扩展解析器
const serviceLoader = require('./loader/service'); //服务解析器
const controllerLoader = require('./loader/controller'); //控制器解析器
const middlewareLoader = require('./loader/middleware'); //中间件解析器
const routerSchemaLoader = require('./loader/router-schema'); //路由参数校验解析器
const routerLoader = require('./loader/router'); //路由解析器
module.exports = {
    /**
     * ElpisCore启动函数(主入口)
     * @param {*} options 项目配置
     * options = {
     * name //项目名称
     * homePath //项目主页路径
     * }
     */
    start(options = {}) {
        const app = new Koa(); //初始化Koa实例

        /**
         * 配置调整
         */
        app.options = options;
        app.baseDir = process.cwd(); //基础路径
        app.businessPath = path.join(app.baseDir, 'app'); //业务代码路径
        app.env = env(); //环境变量
        console.log('elpis-core running in environment: ' + app.env.get());

        //调用各个loader
        configLoader(app);
        console.log('[start] load config loader success');
        extendLoader(app);
        console.log('[start] load extend loader success');
        serviceLoader(app);
        console.log('[start] load service loader success');
        controllerLoader(app);
        console.log('[start] load controller loader success');
        middlewareLoader(app);
        console.log('[start] load middleware success');
        routerSchemaLoader(app);
        console.log('[start] load router schema loader success');

        //注册elpis全局中间件
        const elpisMiddlewarePath = path.resolve(__dirname, '..', 'app', 'middleware.js');
        try {
            require(elpisMiddlewarePath)(app)
            console.log('[start] load global elpis middleware done');
        }
        catch (err) {
            console.error('[exception] there is no global elpis middleware file.')
        }


        //注册业务全局中间件
        try {
            require(path.resolve(app.businessPath, 'middleware.js'))(app)
            console.log('[start] load global business middleware done');
        }
        catch (err) {
            console.error('[exception] there is no global business middleware file.')
        }
        routerLoader(app);
        console.log('[start] load router loader success');
        /**
         * try catch 捕获,并启动监听
         */
        try {
            const port = process.env.PORT || 8080;
            const host = process.env.HOST || '0.0.0.0';
            app.listen(port, host);
            console.log(`Server running at http://${host}:${port}/`);
        }
        catch (err) {
            console.error(err);
        }
        return app;
    }
}

这个章节其实所想引申出的框架改造思想就是:在抽离NPM包的时候,需要考虑清楚__dirname以及process.cwd()等目录写法的区别,其中__dirname通常都是静态变量,不由运行时决定,由自身文件所处的目录路径决定。但是process.cwd()是运行时写法,由执行node命令的项目路径决定。另外就是在重构过程中,不能仅仅依靠报错信息来决定这个位置是否重构,而是需要站在拓展性的角度考量,究竟自己的框架是只应该从用户端读取文件,还是应该只读取自己的项目文件,亦或者是都有

  1. 前端工程化重构 和后端loader的重构类似,之前我们在部署前端工程,读取项目配置的时候只读取了我们框架内部本身的配置,而没有考虑到用户给出的配置项。同时,webpack内部的配置项路径也有一定问题,因为用户内部可能并没有安装对应的node_modules依赖在引入loader亦或者是插件的时候,都会抛出出现找不到依赖的错误。因此这个章节的重构也是围绕着这两个部分在做的。

3.1 路径错误的修复

webpack.base.js中,我们的多入口的构建原来只读取了我们项目内部的pages入口。现在需改造为'项目内部入口' + '用户的业务代码pages入口',具体改动如下

const entryList = path.resolve(process.cwd(), 'app', 'pages', '**', 'entry.*.js')
glob.sync(entryList).forEach((filePath) => {
    const entryName = path.basename(filePath, '.js') //拿到页面文件名称
    pageEntries[entryName] = filePath;
    htmlWebpackPluginList.push(new HTMLWebpackPlugin({
        //产物(最终模板)输出路径
        filename: path.resolve(process.cwd(), 'app', 'public', 'dist', `${entryName}.tpl`),
        //指定要使用的模板文件
        template: path.resolve(process.cwd(), 'app', 'view', 'entry.tpl'),
        //要注入的代码块,注意需要与entry中的chunks保持一致,一个入口需要声明一个HTMLWebpackPlugin,除非代码块需要注入多个entry
        chunks: [entryName]
    }))
})

增加用户业务代码入口后(原push WebpackPlugin的逻辑封装为了handleFile,并区分了业务页面和框架内置页面 elpisEntryList, businessEntryList

function handleFile(filePath, entries = {}, htmlWebpackPluginList = []) {
    const entryName = path.basename(filePath, '.js') //拿到页面文件名称
    entries[entryName] = filePath;
    htmlWebpackPluginList.push(new HTMLWebpackPlugin({
        //产物(最终模板)输出路径
        filename: path.resolve(process.cwd(), 'app', 'public', 'dist', `${entryName}.tpl`),
        //指定要使用的模板文件
        template: path.resolve(__dirname, '..', '..', 'view', 'entry.tpl'),
        //要注入的代码块,注意需要与entry中的chunks保持一致,一个入口需要声明一个HTMLWebpackPlugin,除非代码块需要注入多个entry
        chunks: [entryName]
    }))
}

const elpisEntryList = path.resolve(elpisPagesPath, '**', 'entry.*.js')
const elpisPageEntries = {};
const elpisHtmlWebpackPluginList = []
glob.sync(elpisEntryList).forEach((filePath) => handleFile(filePath, elpisPageEntries, elpisHtmlWebpackPluginList))

/**
 * 用户的业务代码的entry和HtmlWebpackPlugin配置的逻辑
 */
const businessEntryList = path.resolve(process.cwd(), 'app', 'pages', '**', 'entry.*.js')
const businessPageEntries = {};
const businessHtmlWebpackPluginList = []
glob.sync(businessEntryList).forEach((filePath) => handleFile(filePath, businessPageEntries, businessHtmlWebpackPluginList))

此外,之前的webpack loader和plugins内部的路径项也需要更改,例如vue-loader

{
    test:/\.vue$/,
    use:{
        loader:'vue-loader'
    }
}

需改为

{
    test:/\.vue$/,
    use:{
        loader:require.resolve('vue-loader')
    }
}

这是因为原来的require是动态路径,它的寻址逻辑是从程序运行时开始,从用户的项目根目录开始,寻找node_modules目录,然后再去寻找对应的依赖项。而require.resolve()方法可将对应的依赖项改写为静态编译时绝对路径,即框架所在的根目录下的node_modules所在路径。 同理,路径别名也是需要使用require.resolve更换的:

更换前:
//配置模块解析的具体行为(定义webpack在打包时如何找到并解析具体模块的路径)
    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'),
        }
 },
 
 更换后
  //配置模块解析的具体行为(定义webpack在打包时如何找到并解析具体模块的路径)
    resolve: {
        extensions: ['.js', '.vue', '.less', '.css'],
        alias: {
            vue: require.resolve('vue'),
            $elpisPages: elpisPagesPath,
            $elpisCommon: path.resolve(elpisPagesPath, 'common'),
            $elpisCurl: path.resolve(elpisPagesPath, 'common', 'curl.js'),
            $elpisUtils: path.resolve(elpisPagesPath, 'common', 'utils.js'),
            $elpisWidgets: path.resolve(elpisPagesPath, 'widgets'),
            $elpisHeaderContainer: path.resolve(elpisPagesPath, 'widgets', 'header-container', 'header-container.vue'),
            $elpisSiderContainer: path.resolve(elpisPagesPath, 'widgets', 'sider-container', 'sider-container.vue'),
            $elpisSchemaTable: path.resolve(elpisPagesPath, 'widgets', 'schema-table', 'schema-table.vue'),
            $elpisSchemaForm: path.resolve(elpisPagesPath, 'widgets', 'schema-form', 'schema-form.vue'),
            $elpisSchemaSearchBar: path.resolve(elpisPagesPath, 'widgets', 'schema-search-bar', 'schema-search-bar.vue'),
            $elpisStore: path.resolve(elpisPagesPath, 'store'),
            $elpisBoot: path.resolve(elpisPagesPath, 'boot.js'),
            $businessDashboardRouterConfig: path.resolve(process.cwd(), 'app', 'pages', 'dashboard', 'router.js'),
            $businessComponentConfig: path.resolve(process.cwd(), 'app', 'pages', 'dashboard', 'complex-view', 'schema-view', 'components', 'component-config.js'),
            $businessFormItemConfig: path.resolve(process.cwd(), 'app', 'pages', 'widgets', 'schema-form', 'form-item-config.js'),
            $businessSearchItemConfig: path.resolve(process.cwd(), 'app', 'pages', 'widgets', 'schema-search-bar', 'schema-item-config.js'),
        }
    },

这样才能防止源码中import的时候不会出现路径找不到的问题。

devServer中也需要更改热更新插件的依赖寻址

更改前
baseConfig.entry[key] = [
            //主入口文件
            baseConfig.entry[key],
            `webpack-hot-middleware/client?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMR_PATH}&timeout=${DEV_SERVER_CONFIG.TIMEOUT}&reload=true`
        ]
        
        
更改后(require.resolve)
baseConfig.entry[key] = [
            //主入口文件
            baseConfig.entry[key],
            `${require.resolve('webpack-hot-middleware/client')}?path=http://${DEV_SERVER_CONFIG.HOST}:${DEV_SERVER_CONFIG.PORT}/${DEV_SERVER_CONFIG.HMR_PATH}&timeout=${DEV_SERVER_CONFIG.TIMEOUT}&reload=true`
        ]

3.2 用户配置的读取

核心在于利用webpack的smartMerge方式,让它自行判断我们和用户的webpack配置项哪些合并哪些不合并。 其次,就是要使用try catch接住用户的webpack config, 因为用户很可能是忘了定义,亦或者是在目录里直接就没写,用的是我们webpack的默认定义,这种情况会报错。

//加载业务的webpack config
let businessWebpackConfig = {};
try {
    businessWebpackConfig = require(path.resolve(process.cwd(), 'app', 'webpack.config.js'))
} catch (e) {

}

merge.smart({
...elpis的webpack config
}, businessWebpackConfig)
  1. 源码层面的改动

    4.1 依赖路径的修改

    诸如之前的公共库,import xxx from common等,都要统统以我们在工程化中命名好的别名路径做区分例如:

    dashboard.vue
    原: import $curl from 'common/curl.js'
    改为:import $curl from '$elpisCommon/curl.js'
    

    4.2 路由的修改

    由于我们内部配置的路由只适应于自己的页面,而没法支持用户拓展他们的页面。因此这个位置需要集中处理一下

    entry.dashboard.js
    let $businessDashboardRouterConfig = {};
    try {
        $businessDashboardRouterConfig = await require('$businessDashboardRouterConfig').default
    }
    catch (e) {
    
    }
    if (typeof $businessDashboardRouterConfig === 'function') {
    $businessDashboardRouterConfig({
        routes,
        siderRoutes,
    })
    }
    
    

    关键处理点在于使用require运行时导入,因为用户的路由只有在服务器运行起来,且浏览器访问页面时才能够获取到,这一点是import方法所办不到的(因为我们无法从我们的项目import用户的项目路径)。同时也要trycatch处理,因为用户的路由配置文件不一定存在。

    4.3 动态组件的修改

    在对应的schema-view以及公共组件库的目录下改写component-config.js,保证其能够既读取用户自行写的组件,也可以读取我们的封装组件,同理需要 require引入,try catch处理。

    import CreateForm from "./create-form/create-form.vue";
    import DetailPanel from "./detail-panel/detail-panel.vue";
    import EditForm from "./edit-form/edit-form.vue";
    let businessComponentConfig = {};
    try {
        businessComponentConfig = await require('$businessComponentConfig').default
    }
    catch (e) {
    
    }
    export default {
        createForm: {
            component: CreateForm,
        },
        editForm: {
            component: EditForm,
        },
        detailPanel: {
            component: DetailPanel
        },
        ...businessComponentConfig,
    }
    

到了这里,整个项目的发布前重构基本上就已经完成了,我们总结一下一些关键点和疑难点

  1. 重构的首要关键点是在于暴露所需要开放的能力,而非所有能力。如何判断、设计需要暴露的能力才是其中关键点和难点。这个问题通常需要回归我们框架的设计目标做拆分后决定。当然暴露的能力也不是一成不变的,在未来迭代中还可以持续增加。
  2. 当决定好暴露所开放的能力后,书写在index.js中,以函数或者对象的形式暴露出去。为什么是index.js?因为别人在引入你的项目的时候,入口就是你包里面的index.js文件。
  3. 根据你所定义的暴露能力,自行在本地npm link一下,通过另一个项目模拟导入你包的场景,并本地联调。在本地联调中,根据报错信息去到对应的框架目录/源码处做修改,这样效率最高
  4. 本地联调时的报错信息基本上都是路径出错所导致的,因此大部分重构也是围绕着路径重构所开展的,在这一步需要分清楚什么时候用运行时路径(例如process.cwd)什么时候用静态路径__dirname,require.resolve等。
  5. 在重构过程中,不要仅仅只考量报错的地方。在框架拓展性方面也需要有所考量,例如配置是取自己的还是用户的?配置出现了冲突如何解决等问题,都是很重要的考量点。在读取用户配置的时候,基本上只能通过动态运行时require来获取同时要注意用try catch捕获一下,预防报错在合并配置的时候,优先用用户的配置来合并自己的配置

二、文档编写 + 依赖整理

文档编写目的在于告知用户这个项目是做什么的,然后它的启动命令是什么。最主要的是需要告知用户,配置项有哪些(对于我们的项目而言,领域模型的配置尤其重要)。不过文档也不是一蹴而就的,在初代版本里面,只要包括一些基础的启动命令和配置项基本上就够用了。例子参考 www.npmjs.com/package/@ri…

依赖整理:需要将某些对应的开发者依赖(dev-dependencies) 移入到 dependencies 中,这是因为在开发框架时,可能这些依赖并不影响项目源码的正常运行。但是在变成框架供给别人使用的时候,就需要考虑这部分内容了。因为别人在导入我们的npm包的时候,依赖的目录是从我们框架下的node_modules去寻找的,也就是说用户在下载我们的包的时候,需要将我们包运行所需要的依赖一并下载过来。这就是为什么需要做依赖整理的原因

三、发布前package.json编写

  1. 名称的编写(name):这个是很重要的一点,最好在发布前去npm镜像源中搜索一下同名的包是否已经存在。如果已经存在,后续发布是会报错的。如果存在同名的包,那么最好的做法是加一个命名空间在前边,以@符号开头。例如我的elpis包已经有重复项了,那么我可以用我的npm账号名称做命名空间,即@richardzhili/elpis。这个命名空间必须跟自己的npm账户名保持一致 2.版本号的制定,注意每次发布前版本号要比上次版本号加一,版本冲突也会导致发包失败 3.删除之前调试用的scripts

四、执行发布

  1. 确保自己的npm镜像源是官方源npm config get registry,如果不是的话执行一下npm config set registry 重置镜像源
  2. 确保自己的npm账号已登陆,npm whoami,返回结果如果和npm网站上的账号一致就没问题了
  3. 一切都准备就绪后执行npm publish 如果是第一次执行需要npm publish --access public 参数

最后贴上发布镜像源:www.npmjs.com/package/@ri…

❌
❌